Skip to content

Commit

Permalink
Merge pull request #9893 from Icinga/do-not-re-notify-if-filtered-sta…
Browse files Browse the repository at this point in the history
…tes-don-t-change-4503

Discard likely duplicate problem notifications via Notification#last_notified_state_per_user
  • Loading branch information
Al2Klimov authored Dec 13, 2023
2 parents 871fa67 + 97cd05d commit 953eeba
Show file tree
Hide file tree
Showing 8 changed files with 346 additions and 1 deletion.
70 changes: 70 additions & 0 deletions doc/19-technical-concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -1514,6 +1514,76 @@ Message updates will be dropped when:
* Notification does not exist.
* Origin endpoint's zone is not allowed to access this checkable.
#### event::UpdateLastNotifiedStatePerUser <a id="technical-concepts-json-rpc-messages-event-updatelastnotifiedstateperuser"></a>
> Location: `clusterevents.cpp`
##### Message Body
Key | Value
----------|---------
jsonrpc | 2.0
method | event::UpdateLastNotifiedStatePerUser
params | Dictionary
##### Params
Key | Type | Description
-------------|--------|------------------
notification | String | Notification name
user | String | User name
state | Number | Checkable state the user just got a problem notification for
Used to sync the state of a notification object within the same HA zone.
##### Functions
Event Sender: `Notification::OnLastNotifiedStatePerUserUpdated`
Event Receiver: `LastNotifiedStatePerUserUpdatedAPIHandler`
##### Permissions
The receiver will not process messages from not configured endpoints.
Message updates will be dropped when:
* Notification does not exist.
* Origin endpoint is not within the local zone.
#### event::ClearLastNotifiedStatePerUser <a id="technical-concepts-json-rpc-messages-event-clearlastnotifiedstateperuser"></a>
> Location: `clusterevents.cpp`
##### Message Body
Key | Value
----------|---------
jsonrpc | 2.0
method | event::ClearLastNotifiedStatePerUser
params | Dictionary
##### Params
Key | Type | Description
-------------|--------|------------------
notification | String | Notification name
Used to sync the state of a notification object within the same HA zone.
##### Functions
Event Sender: `Notification::OnLastNotifiedStatePerUserCleared`
Event Receiver: `LastNotifiedStatePerUserClearedAPIHandler`
##### Permissions
The receiver will not process messages from not configured endpoints.
Message updates will be dropped when:
* Notification does not exist.
* Origin endpoint is not within the local zone.
#### event::SetForceNextCheck <a id="technical-concepts-json-rpc-messages-event-setforcenextcheck"></a>
> Location: `clusterevents.cpp`
Expand Down
113 changes: 113 additions & 0 deletions lib/icinga/clusterevents.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ REGISTER_APIFUNCTION(SetStateBeforeSuppression, event, &ClusterEvents::StateBefo
REGISTER_APIFUNCTION(SetSuppressedNotifications, event, &ClusterEvents::SuppressedNotificationsChangedAPIHandler);
REGISTER_APIFUNCTION(SetSuppressedNotificationTypes, event, &ClusterEvents::SuppressedNotificationTypesChangedAPIHandler);
REGISTER_APIFUNCTION(SetNextNotification, event, &ClusterEvents::NextNotificationChangedAPIHandler);
REGISTER_APIFUNCTION(UpdateLastNotifiedStatePerUser, event, &ClusterEvents::LastNotifiedStatePerUserUpdatedAPIHandler);
REGISTER_APIFUNCTION(ClearLastNotifiedStatePerUser, event, &ClusterEvents::LastNotifiedStatePerUserClearedAPIHandler);
REGISTER_APIFUNCTION(SetForceNextCheck, event, &ClusterEvents::ForceNextCheckChangedAPIHandler);
REGISTER_APIFUNCTION(SetForceNextNotification, event, &ClusterEvents::ForceNextNotificationChangedAPIHandler);
REGISTER_APIFUNCTION(SetAcknowledgement, event, &ClusterEvents::AcknowledgementSetAPIHandler);
Expand All @@ -50,6 +52,8 @@ void ClusterEvents::StaticInitialize()
Checkable::OnSuppressedNotificationsChanged.connect(&ClusterEvents::SuppressedNotificationsChangedHandler);
Notification::OnSuppressedNotificationsChanged.connect(&ClusterEvents::SuppressedNotificationTypesChangedHandler);
Notification::OnNextNotificationChanged.connect(&ClusterEvents::NextNotificationChangedHandler);
Notification::OnLastNotifiedStatePerUserUpdated.connect(&ClusterEvents::LastNotifiedStatePerUserUpdatedHandler);
Notification::OnLastNotifiedStatePerUserCleared.connect(&ClusterEvents::LastNotifiedStatePerUserClearedHandler);
Checkable::OnForceNextCheckChanged.connect(&ClusterEvents::ForceNextCheckChangedHandler);
Checkable::OnForceNextNotificationChanged.connect(&ClusterEvents::ForceNextNotificationChangedHandler);
Checkable::OnNotificationsRequested.connect(&ClusterEvents::SendNotificationsHandler);
Expand Down Expand Up @@ -529,6 +533,115 @@ Value ClusterEvents::NextNotificationChangedAPIHandler(const MessageOrigin::Ptr&
return Empty;
}

void ClusterEvents::LastNotifiedStatePerUserUpdatedHandler(const Notification::Ptr& notification, const String& user, uint_fast8_t state, const MessageOrigin::Ptr& origin)
{
auto listener (ApiListener::GetInstance());

if (!listener) {
return;
}

Dictionary::Ptr params = new Dictionary();
params->Set("notification", notification->GetName());
params->Set("user", user);
params->Set("state", state);

Dictionary::Ptr message = new Dictionary();
message->Set("jsonrpc", "2.0");
message->Set("method", "event::UpdateLastNotifiedStatePerUser");
message->Set("params", params);

listener->RelayMessage(origin, notification, message, true);
}

Value ClusterEvents::LastNotifiedStatePerUserUpdatedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
{
auto endpoint (origin->FromClient->GetEndpoint());

if (!endpoint) {
Log(LogNotice, "ClusterEvents")
<< "Discarding 'last notified state of user updated' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";

return Empty;
}

if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
Log(LogNotice, "ClusterEvents")
<< "Discarding 'last notified state of user updated' message from '"
<< origin->FromClient->GetIdentity() << "': Unauthorized access.";

return Empty;
}

auto notification (Notification::GetByName(params->Get("notification")));

if (!notification) {
return Empty;
}

auto state (params->Get("state"));

if (!state.IsNumber()) {
return Empty;
}

notification->GetLastNotifiedStatePerUser()->Set(params->Get("user"), state);
Notification::OnLastNotifiedStatePerUserUpdated(notification, params->Get("user"), state, origin);

return Empty;
}

void ClusterEvents::LastNotifiedStatePerUserClearedHandler(const Notification::Ptr& notification, const MessageOrigin::Ptr& origin)
{
auto listener (ApiListener::GetInstance());

if (!listener) {
return;
}

Dictionary::Ptr params = new Dictionary();
params->Set("notification", notification->GetName());

Dictionary::Ptr message = new Dictionary();
message->Set("jsonrpc", "2.0");
message->Set("method", "event::ClearLastNotifiedStatePerUser");
message->Set("params", params);

listener->RelayMessage(origin, notification, message, true);
}

Value ClusterEvents::LastNotifiedStatePerUserClearedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
{
auto endpoint (origin->FromClient->GetEndpoint());

if (!endpoint) {
Log(LogNotice, "ClusterEvents")
<< "Discarding 'last notified state of user cleared' message from '"
<< origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";

return Empty;
}

if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
Log(LogNotice, "ClusterEvents")
<< "Discarding 'last notified state of user cleared' message from '"
<< origin->FromClient->GetIdentity() << "': Unauthorized access.";

return Empty;
}

auto notification (Notification::GetByName(params->Get("notification")));

if (!notification) {
return Empty;
}

notification->GetLastNotifiedStatePerUser()->Clear();
Notification::OnLastNotifiedStatePerUserCleared(notification, origin);

return Empty;
}

void ClusterEvents::ForceNextCheckChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin)
{
ApiListener::Ptr listener = ApiListener::GetInstance();
Expand Down
6 changes: 6 additions & 0 deletions lib/icinga/clusterevents.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ class ClusterEvents
static void NextNotificationChangedHandler(const Notification::Ptr& notification, const MessageOrigin::Ptr& origin);
static Value NextNotificationChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);

static void LastNotifiedStatePerUserUpdatedHandler(const Notification::Ptr& notification, const String& user, uint_fast8_t state, const MessageOrigin::Ptr& origin);
static Value LastNotifiedStatePerUserUpdatedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);

static void LastNotifiedStatePerUserClearedHandler(const Notification::Ptr& notification, const MessageOrigin::Ptr& origin);
static Value LastNotifiedStatePerUserClearedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);

static void ForceNextCheckChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin);
static Value ForceNextCheckChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);

Expand Down
35 changes: 35 additions & 0 deletions lib/icinga/notification.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ std::map<String, int> Notification::m_StateFilterMap;
std::map<String, int> Notification::m_TypeFilterMap;

boost::signals2::signal<void (const Notification::Ptr&, const MessageOrigin::Ptr&)> Notification::OnNextNotificationChanged;
boost::signals2::signal<void (const Notification::Ptr&, const String&, uint_fast8_t, const MessageOrigin::Ptr&)> Notification::OnLastNotifiedStatePerUserUpdated;
boost::signals2::signal<void (const Notification::Ptr&, const MessageOrigin::Ptr&)> Notification::OnLastNotifiedStatePerUserCleared;

String NotificationNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const
{
Expand Down Expand Up @@ -231,6 +233,13 @@ void Notification::BeginExecuteNotification(NotificationType type, const CheckRe
<< "notifications of type '" << notificationTypeName
<< "' for notification object '" << notificationName << "'.";

if (type == NotificationRecovery) {
auto states (GetLastNotifiedStatePerUser());

states->Clear();
OnLastNotifiedStatePerUserCleared(this, nullptr);
}

Checkable::Ptr checkable = GetCheckable();

if (!force) {
Expand Down Expand Up @@ -439,6 +448,22 @@ void Notification::BeginExecuteNotification(NotificationType type, const CheckRe
}
}

if (type == NotificationProblem && !reminder && !checkable->GetVolatile()) {
auto [host, service] = GetHostService(checkable);
uint_fast8_t state = service ? service->GetState() : host->GetState();

if (state == (uint_fast8_t)GetLastNotifiedStatePerUser()->Get(userName)) {
auto stateStr (service ? NotificationServiceStateToString(service->GetState()) : NotificationHostStateToString(host->GetState()));

Log(LogNotice, "Notification")
<< "Notification object '" << notificationName << "': We already notified user '" << userName << "' for a " << stateStr
<< " problem. Likely after that another state change notification was filtered out by config. Not sending duplicate '"
<< stateStr << "' notification.";

continue;
}
}

Log(LogInformation, "Notification")
<< "Sending " << (reminder ? "reminder " : "") << "'" << NotificationTypeToString(type) << "' notification '"
<< notificationName << "' for user '" << userName << "'";
Expand All @@ -452,6 +477,16 @@ void Notification::BeginExecuteNotification(NotificationType type, const CheckRe
/* collect all notified users */
allNotifiedUsers.insert(user);

if (type == NotificationProblem) {
auto [host, service] = GetHostService(checkable);
uint_fast8_t state = service ? service->GetState() : host->GetState();

if (state != (uint_fast8_t)GetLastNotifiedStatePerUser()->Get(userName)) {
GetLastNotifiedStatePerUser()->Set(userName, state);
OnLastNotifiedStatePerUserUpdated(this, userName, state, nullptr);
}
}

/* store all notified users for later recovery checks */
if (type == NotificationProblem && !notifiedProblemUsers->Contains(userName))
notifiedProblemUsers->Add(userName);
Expand Down
4 changes: 3 additions & 1 deletion lib/icinga/notification.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "remote/endpoint.hpp"
#include "remote/messageorigin.hpp"
#include "base/array.hpp"
#include <cstdint>

namespace icinga
{
Expand Down Expand Up @@ -92,6 +93,8 @@ class Notification final : public ObjectImpl<Notification>
static String NotificationHostStateToString(HostState state);

static boost::signals2::signal<void (const Notification::Ptr&, const MessageOrigin::Ptr&)> OnNextNotificationChanged;
static boost::signals2::signal<void (const Notification::Ptr&, const String&, uint_fast8_t, const MessageOrigin::Ptr&)> OnLastNotifiedStatePerUserUpdated;
static boost::signals2::signal<void (const Notification::Ptr&, const MessageOrigin::Ptr&)> OnLastNotifiedStatePerUserCleared;

void Validate(int types, const ValidationUtils& utils) override;

Expand All @@ -105,7 +108,6 @@ class Notification final : public ObjectImpl<Notification>
static const std::map<String, int>& GetStateFilterMap();
static const std::map<String, int>& GetTypeFilterMap();

protected:
void OnConfigLoaded() override;
void OnAllConfigLoaded() override;
void Start(bool runtimeCreated) override;
Expand Down
4 changes: 4 additions & 0 deletions lib/icinga/notification.ti
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ class Notification : CustomVarObject < NotificationNameComposer
default {{{ return 0; }}}
};

[state, no_user_view, no_user_modify] Dictionary::Ptr last_notified_state_per_user {
default {{{ return new Dictionary(); }}}
};

[config, navigation] name(Endpoint) command_endpoint (CommandEndpointRaw) {
navigate {{{
return Endpoint::GetByName(GetCommandEndpointRaw());
Expand Down
5 changes: 5 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ add_boost_test(base
icinga_notification/strings
icinga_notification/state_filter
icinga_notification/type_filter
icinga_notification/no_filter_problem_no_duplicate
icinga_notification/filter_problem_no_duplicate
icinga_notification/volatile_filter_problem_duplicate
icinga_notification/no_recovery_filter_no_duplicate
icinga_notification/recovery_filter_duplicate
icinga_macros/simple
icinga_legacytimeperiod/simple
icinga_legacytimeperiod/advanced
Expand Down
Loading

0 comments on commit 953eeba

Please sign in to comment.