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

Discard likely duplicate problem notifications via Notification#last_notified_state_per_user #9893

Merged
Merged
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
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");
yhabteab marked this conversation as resolved.
Show resolved Hide resolved
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)) {
julianbrost marked this conversation as resolved.
Show resolved Hide resolved
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
Loading