Merge pull request #12616 from AndriiLandiak/notifications-on-resource-shortage

Notification on resources shortage
This commit is contained in:
Viacheslav Klimov 2025-05-30 15:33:50 +03:00 committed by GitHub
commit f65fa52c29
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 503 additions and 37 deletions

View File

@ -116,6 +116,7 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createDatabaseIndexes();
// TODO: cleanup update code after each release
systemDataLoaderService.updateDefaultNotificationConfigs(false);
// Runs upgrade scripts that are not possible in plain SQL.
dataUpdateService.updateData();

View File

@ -0,0 +1,50 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.notification.rule.trigger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.notification.info.ResourcesShortageNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.ResourcesShortageTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.config.ResourcesShortageNotificationRuleTriggerConfig;
@Service
@RequiredArgsConstructor
public class ResourcesShortageTriggerProcessor implements NotificationRuleTriggerProcessor<ResourcesShortageTrigger, ResourcesShortageNotificationRuleTriggerConfig> {
@Override
public boolean matchesFilter(ResourcesShortageTrigger trigger, ResourcesShortageNotificationRuleTriggerConfig triggerConfig) {
float usagePercent = trigger.getUsage() / 100.0f;
return switch (trigger.getResource()) {
case CPU -> usagePercent >= triggerConfig.getCpuThreshold();
case RAM -> usagePercent >= triggerConfig.getRamThreshold();
case STORAGE -> usagePercent >= triggerConfig.getStorageThreshold();
};
}
@Override
public RuleOriginatedNotificationInfo constructNotificationInfo(ResourcesShortageTrigger trigger) {
return ResourcesShortageNotificationInfo.builder().resource(trigger.getResource().name()).usage(trigger.getUsage()).build();
}
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.RESOURCES_SHORTAGE;
}
}

View File

@ -39,6 +39,9 @@ import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.notification.rule.trigger.ResourcesShortageTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.ResourcesShortageTrigger.Resource;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.stats.TbApiUsageStateClient;
import org.thingsboard.server.dao.domain.DomainService;
@ -92,6 +95,7 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
private final DomainService domainService;
private final MailService mailService;
private final SmsService smsService;
private final NotificationRuleProcessor notificationRuleProcessor;
private volatile ScheduledExecutorService scheduler;
@Value("${metrics.system_info.persist_frequency:60}")
@ -163,7 +167,7 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
if (twoFaSettings != null) {
var providers = twoFaSettings.getJsonValue().get("providers");
if (providers != null) {
return providers.size() > 0;
return !providers.isEmpty();
}
}
return false;
@ -180,6 +184,11 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
private void saveCurrentClusterSystemInfo() {
long ts = System.currentTimeMillis();
List<SystemInfoData> clusterSystemData = getSystemData(serviceInfoProvider.getServiceInfo());
clusterSystemData.forEach(data -> {
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.CPU).usage(data.getCpuUsage()).build());
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.RAM).usage(data.getMemoryUsage()).build());
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.STORAGE).usage(data.getDiscUsage()).build());
});
BasicTsKvEntry clusterDataKv = new BasicTsKvEntry(ts, new JsonDataEntry("clusterSystemData", JacksonUtil.toString(clusterSystemData)));
doSave(Arrays.asList(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", true)), clusterDataKv));
}
@ -188,9 +197,21 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
long ts = System.currentTimeMillis();
List<TsKvEntry> tsList = new ArrayList<>();
tsList.add(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", false)));
getCpuUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuUsage", (long) v))));
getMemoryUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("memoryUsage", (long) v))));
getDiscSpaceUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("discUsage", (long) v))));
getCpuUsage().ifPresent(v -> {
long value = (long) v;
tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuUsage", value)));
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.CPU).usage(value).build());
});
getMemoryUsage().ifPresent(v -> {
long value = (long) v;
tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("memoryUsage", value)));
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.RAM).usage(value).build());
});
getDiscSpaceUsage().ifPresent(v -> {
long value = (long) v;
tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("discUsage", value)));
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.STORAGE).usage(value).build());
});
getCpuCount().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuCount", (long) v))));
getTotalMemory().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("totalMemory", v))));
@ -244,4 +265,5 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
scheduler = null;
}
}
}

View File

@ -68,6 +68,8 @@ import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.ResourcesShortageTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.ResourcesShortageTrigger.Resource;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig;
@ -78,6 +80,7 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.Entit
import org.thingsboard.server.common.data.notification.rule.trigger.config.NewPlatformVersionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.config.RateLimitsNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.ResourcesShortageNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.AffectedTenantAdministratorsFilter;
import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter;
@ -91,6 +94,7 @@ import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.controller.TbTestWebSocketClient;
import org.thingsboard.server.dao.notification.DefaultNotifications;
import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.rule.RuleChainService;
@ -98,8 +102,10 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.queue.notification.DefaultNotificationDeduplicationService;
import org.thingsboard.server.service.notification.rule.cache.DefaultNotificationRulesCache;
import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.system.DefaultSystemInfoService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -134,6 +140,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
@SpyBean
private AlarmSubscriptionService alarmSubscriptionService;
@Autowired
private DefaultSystemInfoService systemInfoService;
@Autowired
private NotificationRequestService notificationRequestService;
@Autowired
private RateLimitService rateLimitService;
@ -254,7 +262,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
assertThat(info.getAlarmStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
clients.values().forEach(wsClient -> wsClient.registerWaitForUpdate());
clients.values().forEach(TbTestWebSocketClient::registerWaitForUpdate);
alarmSubscriptionService.acknowledgeAlarm(tenantId, alarm.getId(), System.currentTimeMillis());
AlarmStatus expectedStatus = AlarmStatus.ACTIVE_ACK;
AlarmSeverity expectedSeverity = AlarmSeverity.CRITICAL;
@ -638,7 +646,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
rule = saveNotificationRule(rule);
NotificationRuleInfo ruleInfo = findNotificationRules().getData().get(0);
assertThat(ruleInfo.getId()).isEqualTo(ruleInfo.getId());
assertThat(ruleInfo.getId()).isEqualTo(rule.getId());
assertThat(ruleInfo.getTemplateName()).isEqualTo(template.getName());
assertThat(ruleInfo.getDeliveryMethods()).containsOnly(deliveryMethods);
}
@ -780,6 +788,59 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
});
}
@Test
public void testNotificationRuleProcessing_resourcesShortage() throws Exception {
loginSysAdmin();
ResourcesShortageNotificationRuleTriggerConfig triggerConfig = ResourcesShortageNotificationRuleTriggerConfig.builder()
.ramThreshold(0.01f)
.cpuThreshold(1f)
.storageThreshold(1f)
.build();
createNotificationRule(triggerConfig, "Warning: ${resource} shortage", "${resource} shortage", createNotificationTarget(tenantAdminUserId).getId());
loginTenantAdmin();
Method method = DefaultSystemInfoService.class.getDeclaredMethod("saveCurrentMonolithSystemInfo");
method.setAccessible(true);
method.invoke(systemInfoService);
await().atMost(10, TimeUnit.SECONDS).until(() -> getMyNotifications(false, 100).size() == 1);
Notification notification = getMyNotifications(false, 100).get(0);
assertThat(notification.getSubject()).isEqualTo("Warning: RAM shortage");
assertThat(notification.getText()).isEqualTo("RAM shortage");
}
@Test
public void testNotificationsDeduplication_resourcesShortage() throws Exception {
loginSysAdmin();
ResourcesShortageNotificationRuleTriggerConfig triggerConfig = ResourcesShortageNotificationRuleTriggerConfig.builder()
.ramThreshold(0.01f)
.cpuThreshold(1f)
.storageThreshold(1f)
.build();
createNotificationRule(triggerConfig, "Warning: ${resource} shortage", "${resource} shortage", createNotificationTarget(tenantAdminUserId).getId());it a
loginTenantAdmin();
assertThat(getMyNotifications(false, 100)).size().isZero();
for (int i = 0; i < 10; i++) {
notificationRuleProcessor.process(ResourcesShortageTrigger.builder()
.resource(Resource.RAM)
.usage(15L)
.build());
TimeUnit.MILLISECONDS.sleep(300);
}
await().atMost(10, TimeUnit.SECONDS).until(() -> getMyNotifications(false, 100).size() == 1);
Notification notification = getMyNotifications(false, 100).get(0);
assertThat(notification.getSubject()).isEqualTo("Warning: RAM shortage");
assertThat(notification.getText()).isEqualTo("RAM shortage");
// deduplication is 5 minute, no new message is exp
notificationRuleProcessor.process(ResourcesShortageTrigger.builder()
.resource(Resource.RAM)
.usage(5L)
.build());
await("").atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertThat(getMyNotifications(false, 100)).size().isOne());
}
@Test
public void testNotificationRuleDisabling() throws Exception {
EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig();

View File

@ -37,7 +37,8 @@ public enum NotificationType {
RATE_LIMITS,
EDGE_CONNECTION,
EDGE_COMMUNICATION_FAILURE,
TASK_PROCESSING_FAILURE;
TASK_PROCESSING_FAILURE,
RESOURCES_SHORTAGE;
@Getter
private boolean system; // for future use and compatibility with PE

View File

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.notification.info;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ResourcesShortageNotificationInfo implements RuleOriginatedNotificationInfo {
private String resource;
private Long usage;
@Override
public Map<String, String> getTemplateData() {
return Map.of(
"resource", resource,
"usage", String.valueOf(usage)
);
}
}

View File

@ -55,9 +55,4 @@ public class NewPlatformVersionTrigger implements NotificationRuleTrigger {
updateInfo.getCurrentVersion(), updateInfo.getLatestVersion());
}
@Override
public long getDefaultDeduplicationDuration() {
return 0;
}
}

View File

@ -0,0 +1,71 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.io.Serial;
import java.util.concurrent.TimeUnit;
@Data
@Builder
public class ResourcesShortageTrigger implements NotificationRuleTrigger {
@Serial
private static final long serialVersionUID = 6024216015202949570L;
private Resource resource;
private Long usage;
@Override
public TenantId getTenantId() {
return TenantId.SYS_TENANT_ID;
}
@Override
public EntityId getOriginatorEntityId() {
return TenantId.SYS_TENANT_ID;
}
@Override
public boolean deduplicate() {
return true;
}
@Override
public String getDeduplicationKey() {
return resource.name();
}
@Override
public long getDefaultDeduplicationDuration() {
return TimeUnit.HOURS.toMillis(1);
}
@Override
public NotificationRuleTriggerType getType() {
return NotificationRuleTriggerType.RESOURCES_SHORTAGE;
}
public enum Resource {
CPU, RAM, STORAGE
}
}

View File

@ -22,10 +22,15 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.io.Serial;
@Data
@Builder
public class TaskProcessingFailureTrigger implements NotificationRuleTrigger {
@Serial
private static final long serialVersionUID = 5606203770553105345L;
private final HousekeeperTask task;
private final int attempt;
private final Throwable error;
@ -45,9 +50,4 @@ public class TaskProcessingFailureTrigger implements NotificationRuleTrigger {
return task.getEntityId();
}
@Override
public boolean deduplicate() {
return false;
}
}

View File

@ -38,7 +38,8 @@ import java.io.Serializable;
@Type(value = RateLimitsNotificationRuleTriggerConfig.class, name = "RATE_LIMITS"),
@Type(value = EdgeConnectionNotificationRuleTriggerConfig.class, name = "EDGE_CONNECTION"),
@Type(value = EdgeCommunicationFailureNotificationRuleTriggerConfig.class, name = "EDGE_COMMUNICATION_FAILURE"),
@Type(value = TaskProcessingFailureNotificationRuleTriggerConfig.class, name = "TASK_PROCESSING_FAILURE")
@Type(value = TaskProcessingFailureNotificationRuleTriggerConfig.class, name = "TASK_PROCESSING_FAILURE"),
@Type(value = ResourcesShortageNotificationRuleTriggerConfig.class, name = "RESOURCES_SHORTAGE")
})
public interface NotificationRuleTriggerConfig extends Serializable {

View File

@ -32,7 +32,8 @@ public enum NotificationRuleTriggerType {
ENTITIES_LIMIT(false),
API_USAGE_LIMIT(false),
RATE_LIMITS(false),
TASK_PROCESSING_FAILURE(false);
TASK_PROCESSING_FAILURE(false),
RESOURCES_SHORTAGE(false);
private final boolean tenantLevel;

View File

@ -0,0 +1,47 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.notification.rule.trigger.config;
import jakarta.validation.constraints.Max;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ResourcesShortageNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
@Serial
private static final long serialVersionUID = 339395299693241424L;
@Max(1)
private float cpuThreshold; // in percents
@Max(1)
private float ramThreshold; // in percents
@Max(1)
private float storageThreshold; // in percents
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.RESOURCES_SHORTAGE;
}
}

View File

@ -186,6 +186,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
defaultNotifications.create(tenantId, DefaultNotifications.exceededRateLimitsForSysadmin, sysAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.newPlatformVersion, sysAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.taskProcessingFailure, tenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.resourcesShortage, sysAdmins.getId());
return;
}
@ -221,6 +222,9 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
if (!isNotificationConfigured(tenantId, NotificationType.TASK_PROCESSING_FAILURE)) {
defaultNotifications.create(tenantId, DefaultNotifications.taskProcessingFailure, sysAdmins.getId());
}
if (!isNotificationConfigured(tenantId, NotificationType.RESOURCES_SHORTAGE)) {
defaultNotifications.create(tenantId, DefaultNotifications.resourcesShortage, sysAdmins.getId());
}
} else {
var requiredNotificationTypes = List.of(NotificationType.EDGE_CONNECTION, NotificationType.EDGE_COMMUNICATION_FAILURE);
var existingNotificationTypes = notificationTemplateService.findNotificationTemplatesByTenantIdAndNotificationTypes(

View File

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.notification.rule.DefaultNotificationR
import org.thingsboard.server.common.data.notification.rule.EscalatedNotificationRuleRecipientsConfig;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.ResourcesShortageTrigger.Resource;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig;
@ -49,6 +50,7 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.NewPl
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.config.RateLimitsNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.ResourcesShortageNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.TaskProcessingFailureNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
@ -372,6 +374,20 @@ public class DefaultNotifications {
.build())
.build();
public static final DefaultNotification resourcesShortage = DefaultNotification.builder()
.name("Resources shortage notification")
.type(NotificationType.RESOURCES_SHORTAGE)
.subject("Warning: ${resource} shortage")
.text("${resource} usage is at ${usage}%.")
.icon("warning")
.rule(DefaultRule.builder()
.name("Resources shortage")
.triggerConfig(ResourcesShortageNotificationRuleTriggerConfig.builder().cpuThreshold(0.8f).storageThreshold(0.8f).ramThreshold(0.8f).build())
.description("Send notification to system admins on resource shortage")
.build())
.color(RED_COLOR)
.build();
private final NotificationTemplateService templateService;
private final NotificationRuleService ruleService;

View File

@ -480,17 +480,18 @@
ignoreAuthorityFilter
[allowedEntityTypes]="allowEntityTypeForEntitiesLimit">
</tb-entity-type-list>
<div class="limit-slider-container flex flex-row items-center justify-start xs:flex-col xs:items-stretch">
<label translate>notification.threshold</label>
<div class="limit-slider-container mb-4 flex flex-row items-center justify-start xs:flex-col xs:items-stretch">
<span translate>notification.threshold</span>
<div class="flex flex-1 flex-row items-center justify-start">
<mat-slider class="flex-1" min="0" max="1" step="0.01" discrete [displayWith]="formatLabel">
<mat-slider class="flex-1" min="0" max="100" step="1" discrete [displayWith]="formatLabel">
<input matSliderThumb formControlName="threshold">
</mat-slider>
<mat-form-field class="limit-slider-value">
<input matInput formControlName="threshold" type="number" step="0.01"
<mat-form-field class="limit-slider-value" subscriptSizing="dynamic">
<input matInput formControlName="threshold" type="number" step="1"
[value]="entitiesLimitTemplateForm.get('triggerConfig.threshold').value"
min="0"
max="1"/>
max="100"/>
<span class="mr-2" matSuffix>%</span>
</mat-form-field>
</div>
</div>
@ -591,6 +592,69 @@
</section>
</form>
</mat-step>
<mat-step *ngIf="ruleNotificationForm.get('triggerType').value === triggerType.RESOURCES_SHORTAGE"
[stepControl]="resourceUsageShortageTemplateForm">
<ng-template matStepLabel>{{ 'notification.resources-shortage-trigger-settings' | translate }}</ng-template>
<form [formGroup]="resourceUsageShortageTemplateForm">
<fieldset class="fields-group tb-margin" formGroupName="triggerConfig">
<legend translate>notification.filter</legend>
<div class="limit-slider-container mb-4 flex flex-row items-center justify-start xs:flex-col xs:items-stretch">
<span translate>notification.cpu-threshold</span>
<div class="flex flex-1 flex-row items-center justify-start">
<mat-slider class="flex-1" min="0" max="100" step="1" discrete [displayWith]="formatLabel">
<input matSliderThumb formControlName="cpuThreshold">
</mat-slider>
<mat-form-field class="limit-slider-value" subscriptSizing="dynamic">
<input matInput formControlName="cpuThreshold" type="number" step="1"
[value]="resourceUsageShortageTemplateForm.get('triggerConfig.cpuThreshold').value"
min="0"
max="100"/>
<span class="mr-2" matSuffix>%</span>
</mat-form-field>
</div>
</div>
<div class="limit-slider-container mb-4 flex flex-row items-center justify-start xs:flex-col xs:items-stretch">
<span translate>notification.ram-threshold</span>
<div class="flex flex-1 flex-row items-center justify-start">
<mat-slider class="flex-1" min="0" max="100" step="1" discrete [displayWith]="formatLabel">
<input matSliderThumb formControlName="ramThreshold">
</mat-slider>
<mat-form-field class="limit-slider-value" subscriptSizing="dynamic">
<input matInput formControlName="ramThreshold" type="number" step="1"
[value]="resourceUsageShortageTemplateForm.get('triggerConfig.ramThreshold').value"
min="0"
max="100"/>
<span class="mr-2" matSuffix>%</span>
</mat-form-field>
</div>
</div>
<div class="limit-slider-container mb-4 flex flex-row items-center justify-start xs:flex-col xs:items-stretch">
<span translate>notification.storage-threshold</span>
<div class="flex flex-1 flex-row items-center justify-start">
<mat-slider class="flex-1" min="0" max="100" step="1" discrete [displayWith]="formatLabel">
<input matSliderThumb formControlName="storageThreshold">
</mat-slider>
<mat-form-field class="limit-slider-value" subscriptSizing="dynamic">
<input matInput formControlName="storageThreshold" type="number" step="1"
[value]="resourceUsageShortageTemplateForm.get('triggerConfig.storageThreshold').value"
min="0"
max="100"/>
<span class="mr-2" matSuffix>%</span>
</mat-form-field>
</div>
</div>
</fieldset>
</form>
<form [formGroup]="ruleNotificationForm">
<section formGroupName="additionalConfig">
<mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label>
<input matInput formControlName="description">
</mat-form-field>
</section>
</form>
</mat-step>
</mat-horizontal-stepper>
</div>
<mat-divider></mat-divider>

View File

@ -81,7 +81,7 @@
min-width: 364px;
}
.limit-slider-container {
> label {
> span {
margin-right: 16px;
width: min-content;
max-width: 40%;

View File

@ -102,6 +102,7 @@ export class RuleNotificationDialogComponent extends
edgeCommunicationFailureTemplateForm: FormGroup;
edgeConnectionTemplateForm: FormGroup;
taskProcessingFailureTemplateForm: FormGroup;
resourceUsageShortageTemplateForm: FormGroup;
triggerType = TriggerType;
triggerTypes: TriggerType[];
@ -315,7 +316,7 @@ export class RuleNotificationDialogComponent extends
this.entitiesLimitTemplateForm = this.fb.group({
triggerConfig: this.fb.group({
entityTypes: [],
threshold: [.8, [Validators.min(0), Validators.max(1)]]
threshold: [80, [Validators.min(0), Validators.max(100)]]
})
});
@ -344,6 +345,14 @@ export class RuleNotificationDialogComponent extends
})
});
this.resourceUsageShortageTemplateForm = this.fb.group({
triggerConfig: this.fb.group({
cpuThreshold: [80, [Validators.min(0), Validators.max(100)]],
ramThreshold: [80, [Validators.min(0), Validators.max(100)]],
storageThreshold: [80, [Validators.min(0), Validators.max(100)]]
})
});
this.triggerTypeFormsMap = new Map<TriggerType, FormGroup>([
[TriggerType.ALARM, this.alarmTemplateForm],
[TriggerType.ALARM_COMMENT, this.alarmCommentTemplateForm],
@ -357,7 +366,8 @@ export class RuleNotificationDialogComponent extends
[TriggerType.RATE_LIMITS, this.rateLimitsTemplateForm],
[TriggerType.EDGE_COMMUNICATION_FAILURE, this.edgeCommunicationFailureTemplateForm],
[TriggerType.EDGE_CONNECTION, this.edgeConnectionTemplateForm],
[TriggerType.TASK_PROCESSING_FAILURE, this.taskProcessingFailureTemplateForm]
[TriggerType.TASK_PROCESSING_FAILURE, this.taskProcessingFailureTemplateForm],
[TriggerType.RESOURCES_SHORTAGE, this.resourceUsageShortageTemplateForm]
]);
if (data.isAdd || data.isCopy) {
@ -380,6 +390,14 @@ export class RuleNotificationDialogComponent extends
this.deviceInactivityTemplateForm.get('triggerConfig.filterByDevice')
.patchValue(!!this.ruleNotification.triggerConfig.devices, {onlySelf: true});
}
if (this.ruleNotification.triggerType === TriggerType.ENTITIES_LIMIT) {
this.entitiesLimitTemplateForm.get('triggerConfig.threshold').patchValue(this.ruleNotification.triggerConfig.threshold * 100, {emitEvent: false});
}
if (this.ruleNotification.triggerType === TriggerType.RESOURCES_SHORTAGE) {
this.resourceUsageShortageTemplateForm.get('triggerConfig.cpuThreshold').patchValue(this.ruleNotification.triggerConfig.cpuThreshold * 100, {emitEvent: false});
this.resourceUsageShortageTemplateForm.get('triggerConfig.ramThreshold').patchValue(this.ruleNotification.triggerConfig.ramThreshold * 100, {emitEvent: false});
this.resourceUsageShortageTemplateForm.get('triggerConfig.storageThreshold').patchValue(this.ruleNotification.triggerConfig.storageThreshold * 100, {emitEvent: false});
}
}
}
@ -428,6 +446,14 @@ export class RuleNotificationDialogComponent extends
if (triggerType === TriggerType.DEVICE_ACTIVITY) {
delete formValue.triggerConfig.filterByDevice;
}
if (triggerType === TriggerType.ENTITIES_LIMIT) {
formValue.triggerConfig.threshold = formValue.triggerConfig.threshold / 100;
}
if (triggerType === TriggerType.RESOURCES_SHORTAGE) {
formValue.triggerConfig.cpuThreshold = formValue.triggerConfig.cpuThreshold / 100;
formValue.triggerConfig.ramThreshold = formValue.triggerConfig.ramThreshold / 100;
formValue.triggerConfig.storageThreshold = formValue.triggerConfig.storageThreshold / 100;
}
formValue.recipientsConfig.triggerType = triggerType;
formValue.triggerConfig.triggerType = triggerType;
if (this.ruleNotification && !this.data.isCopy) {
@ -483,8 +509,7 @@ export class RuleNotificationDialogComponent extends
}
formatLabel(value: number): string {
const formatValue = (value * 100).toFixed();
return `${formatValue}%`;
return `${value}%`;
}
private isSysAdmin(): boolean {
@ -497,7 +522,8 @@ export class RuleNotificationDialogComponent extends
TriggerType.API_USAGE_LIMIT,
TriggerType.NEW_PLATFORM_VERSION,
TriggerType.RATE_LIMITS,
TriggerType.TASK_PROCESSING_FAILURE
TriggerType.TASK_PROCESSING_FAILURE,
TriggerType.RESOURCES_SHORTAGE
]);
if (this.isSysAdmin()) {

View File

@ -180,7 +180,8 @@ export class TemplateNotificationDialogComponent
NotificationType.API_USAGE_LIMIT,
NotificationType.NEW_PLATFORM_VERSION,
NotificationType.RATE_LIMITS,
NotificationType.TASK_PROCESSING_FAILURE
NotificationType.TASK_PROCESSING_FAILURE,
NotificationType.RESOURCES_SHORTAGE
]);
if (this.isSysAdmin()) {

View File

@ -129,7 +129,7 @@ export interface NotificationRule extends Omit<BaseData<NotificationRuleId>, 'la
export type NotificationRuleTriggerConfig = Partial<AlarmNotificationRuleTriggerConfig & DeviceInactivityNotificationRuleTriggerConfig &
EntityActionNotificationRuleTriggerConfig & AlarmCommentNotificationRuleTriggerConfig & AlarmAssignmentNotificationRuleTriggerConfig &
RuleEngineLifecycleEventNotificationRuleTriggerConfig & EntitiesLimitNotificationRuleTriggerConfig &
ApiUsageLimitNotificationRuleTriggerConfig & RateLimitsNotificationRuleTriggerConfig>;
ApiUsageLimitNotificationRuleTriggerConfig & RateLimitsNotificationRuleTriggerConfig & ResourceUsageShortageNotificationRuleTriggerConfig>;
export interface AlarmNotificationRuleTriggerConfig {
alarmTypes?: Array<string>;
@ -183,6 +183,12 @@ export interface EntitiesLimitNotificationRuleTriggerConfig {
threshold: number;
}
export interface ResourceUsageShortageNotificationRuleTriggerConfig {
cpuThreshold: number;
ramThreshold: number;
storageThreshold: number;
}
export interface ApiUsageLimitNotificationRuleTriggerConfig {
apiFeatures: ApiFeature[];
notifyOn: ApiUsageStateValue[];
@ -526,7 +532,8 @@ export enum NotificationType {
RATE_LIMITS = 'RATE_LIMITS',
EDGE_CONNECTION = 'EDGE_CONNECTION',
EDGE_COMMUNICATION_FAILURE = 'EDGE_COMMUNICATION_FAILURE',
TASK_PROCESSING_FAILURE = 'TASK_PROCESSING_FAILURE'
TASK_PROCESSING_FAILURE = 'TASK_PROCESSING_FAILURE',
RESOURCES_SHORTAGE = 'RESOURCES_SHORTAGE'
}
export const NotificationTypeIcons = new Map<NotificationType, string | null>([
@ -538,7 +545,8 @@ export const NotificationTypeIcons = new Map<NotificationType, string | null>([
[NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, 'settings_ethernet'],
[NotificationType.ENTITIES_LIMIT, 'data_thresholding'],
[NotificationType.API_USAGE_LIMIT, 'insert_chart'],
[NotificationType.TASK_PROCESSING_FAILURE, 'warning']
[NotificationType.TASK_PROCESSING_FAILURE, 'warning'],
[NotificationType.RESOURCES_SHORTAGE, 'warning']
]);
export const AlarmSeverityNotificationColors = new Map<AlarmSeverity, string>(
@ -657,6 +665,12 @@ export const NotificationTemplateTypeTranslateMap = new Map<NotificationType, No
helpId: 'notification/task_processing_failure'
}
],
[NotificationType.RESOURCES_SHORTAGE,
{
name: 'notification.template-type.resources-shortage',
helpId: 'notification/resources_shortage'
}
]
]);
export enum TriggerType {
@ -673,6 +687,7 @@ export enum TriggerType {
EDGE_CONNECTION = 'EDGE_CONNECTION',
EDGE_COMMUNICATION_FAILURE = 'EDGE_COMMUNICATION_FAILURE',
TASK_PROCESSING_FAILURE = 'TASK_PROCESSING_FAILURE',
RESOURCES_SHORTAGE = 'RESOURCES_SHORTAGE'
}
export const TriggerTypeTranslationMap = new Map<TriggerType, string>([
@ -688,7 +703,8 @@ export const TriggerTypeTranslationMap = new Map<TriggerType, string>([
[TriggerType.RATE_LIMITS, 'notification.trigger.rate-limits'],
[TriggerType.EDGE_CONNECTION, 'notification.trigger.edge-connection'],
[TriggerType.EDGE_COMMUNICATION_FAILURE, 'notification.trigger.edge-communication-failure'],
[TriggerType.TASK_PROCESSING_FAILURE, 'notification.trigger.task-processing-failure']
[TriggerType.TASK_PROCESSING_FAILURE, 'notification.trigger.task-processing-failure'],
[TriggerType.RESOURCES_SHORTAGE, 'notification.trigger.resources-shortage']
]);
export interface NotificationUserSettings {

View File

@ -0,0 +1,41 @@
#### Resources shortage notification templatization
<div class="divider"></div>
<br/>
Notification subject and message fields support templatization.
The list of available templatization parameters depends on the template type.
See the available types and parameters below:
Available template parameters:
* `resource` - the resource name;
* `usage` - the resource usage value;
Parameter names must be wrapped using `${...}`. For example: `${resource}`.
You may also modify the value of the parameter with one of the suffixes:
* `upperCase`, for example - `${resource:upperCase}`
* `lowerCase`, for example - `${resource:lowerCase}`
* `capitalize`, for example - `${resource:capitalize}`
<div class="divider"></div>
##### Examples
Let's assume there is a resource usage shortage and the system is low on free resources (CPU, RAM, or Storage).
The following template:
```text
Warning: ${resource} is critically high at ${usage}%
{:copy-code}
```
will be transformed to:
```text
Warning: CPU is critically high at 83%
```
<br>
<br>

View File

@ -3876,6 +3876,7 @@
"new-platform-version-trigger-settings": "New platform version trigger settings",
"rate-limits-trigger-settings": "Exceeded rate limits trigger settings",
"task-processing-failure-trigger-settings": "Task processing failure trigger settings",
"resources-shortage-trigger-settings": "Resources shortage trigger settings",
"at-least-one-should-be-selected": "At least one should be selected",
"basic-settings": "Basic settings",
"button-text": "Button text",
@ -3890,6 +3891,7 @@
"create-new": "Create new",
"created": "Created",
"customize-messages": "Customize messages",
"cpu-threshold": "CPU threshold",
"delete-notification-text": "Be careful, after the confirmation the notification will become unrecoverable.",
"delete-notification-title": "Are you sure you want to delete the notification?",
"delete-notifications-text": "Be careful, after the confirmation notifications will become unrecoverable.",
@ -4005,6 +4007,7 @@
"only-rule-chain-lifecycle-failures": "Only rule chain lifecycle failures",
"only-rule-node-lifecycle-failures": "Only rule node lifecycle failures",
"platform-users": "Platform users",
"ram-threshold": "RAM threshold",
"rate-limits": "Rate limits",
"rate-limits-hint": "If the field is empty, the trigger will be applied to all rate limits",
"recipient": "Recipient",
@ -4070,6 +4073,7 @@
"start-from-scratch": "Start from scratch",
"status": "Status",
"stop-escalation-alarm-status-become": "Stop the escalation on the alarm status become:",
"storage-threshold": "Storage threshold",
"subject": "Subject",
"subject-required": "Subject is required",
"subject-max-length": "Subject should be less than or equal to {{ length }} characters",
@ -4091,7 +4095,8 @@
"rate-limits": "Exceeded rate limits",
"edge-communication-failure": "Edge communication failure",
"edge-connection": "Edge connection",
"task-processing-failure": "Task processing failure"
"task-processing-failure": "Task processing failure",
"resources-shortage": "Resources shortage"
},
"templates": "Templates",
"notification-templates": "Notifications / Templates",
@ -4115,6 +4120,7 @@
"edge-connection": "Edge connection",
"edge-communication-failure": "Edge communication failure",
"task-processing-failure": "Task processing failure",
"resources-shortage": "Resources shortage",
"trigger": "Trigger",
"trigger-required": "Trigger is required"
},