diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 6765e95246..5e5185ac8b 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -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(); diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ResourcesShortageTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ResourcesShortageTriggerProcessor.java new file mode 100644 index 0000000000..aefb628d2d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ResourcesShortageTriggerProcessor.java @@ -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 { + + @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; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java b/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java index 4d2e641a91..adc87957d3 100644 --- a/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java +++ b/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java @@ -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 0; + return !providers.isEmpty(); } } return false; @@ -180,6 +184,11 @@ public class DefaultSystemInfoService extends TbApplicationEventListener 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 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 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(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java index 76ad4ac4dc..d4d3d11e8b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java @@ -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 diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/ResourcesShortageNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/ResourcesShortageNotificationInfo.java new file mode 100644 index 0000000000..24cb21febd --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/ResourcesShortageNotificationInfo.java @@ -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 getTemplateData() { + return Map.of( + "resource", resource, + "usage", String.valueOf(usage) + ); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionTrigger.java index 7a66d430e6..204ae15e57 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionTrigger.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionTrigger.java @@ -55,9 +55,4 @@ public class NewPlatformVersionTrigger implements NotificationRuleTrigger { updateInfo.getCurrentVersion(), updateInfo.getLatestVersion()); } - @Override - public long getDefaultDeduplicationDuration() { - return 0; - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java new file mode 100644 index 0000000000..f12c80d5db --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java @@ -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 + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/TaskProcessingFailureTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/TaskProcessingFailureTrigger.java index 2e781e26f7..3677df13ab 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/TaskProcessingFailureTrigger.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/TaskProcessingFailureTrigger.java @@ -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; - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java index afdc891b86..8db874f812 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java @@ -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 { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java index 72cbb51b0c..aeabfa814d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java @@ -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; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ResourcesShortageNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ResourcesShortageNotificationRuleTriggerConfig.java new file mode 100644 index 0000000000..e8ea6c9be5 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ResourcesShortageNotificationRuleTriggerConfig.java @@ -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; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java index de83fe4df7..ea7d3488b2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java @@ -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( diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java index cb7b281d67..9b8b8b255e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java @@ -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; diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html index b13943067b..68122875c0 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html @@ -480,17 +480,18 @@ ignoreAuthorityFilter [allowedEntityTypes]="allowEntityTypeForEntitiesLimit"> -
- +
+ notification.threshold
- + - - + + max="100"/> + %
@@ -591,6 +592,69 @@ + + + {{ 'notification.resources-shortage-trigger-settings' | translate }} +
+
+ notification.filter +
+ notification.cpu-threshold +
+ + + + + + % + +
+
+
+ notification.ram-threshold +
+ + + + + + % + +
+
+
+ notification.storage-threshold +
+ + + + + + % + +
+
+
+
+
+
+ + notification.description + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.scss index 5e495655f1..9f1221cad5 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.scss @@ -81,7 +81,7 @@ min-width: 364px; } .limit-slider-container { - > label { + > span { margin-right: 16px; width: min-content; max-width: 40%; diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts index 5d1a8b77ac..e0ddefcbe8 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts @@ -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.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()) { diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts index 86d2b651ab..98595de798 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts @@ -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()) { diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts index fad4611b45..63d7edd2a2 100644 --- a/ui-ngx/src/app/shared/models/notification.models.ts +++ b/ui-ngx/src/app/shared/models/notification.models.ts @@ -129,7 +129,7 @@ export interface NotificationRule extends Omit, 'la export type NotificationRuleTriggerConfig = Partial; + ApiUsageLimitNotificationRuleTriggerConfig & RateLimitsNotificationRuleTriggerConfig & ResourceUsageShortageNotificationRuleTriggerConfig>; export interface AlarmNotificationRuleTriggerConfig { alarmTypes?: Array; @@ -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([ @@ -538,7 +545,8 @@ export const NotificationTypeIcons = new Map([ [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( @@ -657,6 +665,12 @@ export const NotificationTemplateTypeTranslateMap = new Map([ @@ -688,7 +703,8 @@ export const TriggerTypeTranslationMap = new Map([ [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 { diff --git a/ui-ngx/src/assets/help/en_US/notification/resources_shortage.md b/ui-ngx/src/assets/help/en_US/notification/resources_shortage.md new file mode 100644 index 0000000000..9b469c0f85 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/notification/resources_shortage.md @@ -0,0 +1,41 @@ +#### Resources shortage notification templatization + +
+
+ +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}` + +
+ +##### 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% +``` + +
+
diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index e3f67fe8f7..14a000aae9 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -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" },