Merge pull request #12616 from AndriiLandiak/notifications-on-resource-shortage
Notification on resources shortage
This commit is contained in:
commit
f65fa52c29
@ -116,6 +116,7 @@ public class ThingsboardInstallService {
|
|||||||
entityDatabaseSchemaService.createDatabaseIndexes();
|
entityDatabaseSchemaService.createDatabaseIndexes();
|
||||||
|
|
||||||
// TODO: cleanup update code after each release
|
// TODO: cleanup update code after each release
|
||||||
|
systemDataLoaderService.updateDefaultNotificationConfigs(false);
|
||||||
|
|
||||||
// Runs upgrade scripts that are not possible in plain SQL.
|
// Runs upgrade scripts that are not possible in plain SQL.
|
||||||
dataUpdateService.updateData();
|
dataUpdateService.updateData();
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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.JsonDataEntry;
|
||||||
import org.thingsboard.server.common.data.kv.LongDataEntry;
|
import org.thingsboard.server.common.data.kv.LongDataEntry;
|
||||||
import org.thingsboard.server.common.data.kv.TsKvEntry;
|
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.msg.queue.ServiceType;
|
||||||
import org.thingsboard.server.common.stats.TbApiUsageStateClient;
|
import org.thingsboard.server.common.stats.TbApiUsageStateClient;
|
||||||
import org.thingsboard.server.dao.domain.DomainService;
|
import org.thingsboard.server.dao.domain.DomainService;
|
||||||
@ -92,6 +95,7 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
|
|||||||
private final DomainService domainService;
|
private final DomainService domainService;
|
||||||
private final MailService mailService;
|
private final MailService mailService;
|
||||||
private final SmsService smsService;
|
private final SmsService smsService;
|
||||||
|
private final NotificationRuleProcessor notificationRuleProcessor;
|
||||||
private volatile ScheduledExecutorService scheduler;
|
private volatile ScheduledExecutorService scheduler;
|
||||||
|
|
||||||
@Value("${metrics.system_info.persist_frequency:60}")
|
@Value("${metrics.system_info.persist_frequency:60}")
|
||||||
@ -163,7 +167,7 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
|
|||||||
if (twoFaSettings != null) {
|
if (twoFaSettings != null) {
|
||||||
var providers = twoFaSettings.getJsonValue().get("providers");
|
var providers = twoFaSettings.getJsonValue().get("providers");
|
||||||
if (providers != null) {
|
if (providers != null) {
|
||||||
return providers.size() > 0;
|
return !providers.isEmpty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -180,6 +184,11 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
|
|||||||
private void saveCurrentClusterSystemInfo() {
|
private void saveCurrentClusterSystemInfo() {
|
||||||
long ts = System.currentTimeMillis();
|
long ts = System.currentTimeMillis();
|
||||||
List<SystemInfoData> clusterSystemData = getSystemData(serviceInfoProvider.getServiceInfo());
|
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)));
|
BasicTsKvEntry clusterDataKv = new BasicTsKvEntry(ts, new JsonDataEntry("clusterSystemData", JacksonUtil.toString(clusterSystemData)));
|
||||||
doSave(Arrays.asList(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", true)), clusterDataKv));
|
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();
|
long ts = System.currentTimeMillis();
|
||||||
List<TsKvEntry> tsList = new ArrayList<>();
|
List<TsKvEntry> tsList = new ArrayList<>();
|
||||||
tsList.add(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", false)));
|
tsList.add(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", false)));
|
||||||
getCpuUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuUsage", (long) v))));
|
getCpuUsage().ifPresent(v -> {
|
||||||
getMemoryUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("memoryUsage", (long) v))));
|
long value = (long) v;
|
||||||
getDiscSpaceUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("discUsage", (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))));
|
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))));
|
getTotalMemory().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("totalMemory", v))));
|
||||||
@ -244,4 +265,5 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
|
|||||||
scheduler = null;
|
scheduler = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.NotificationRuleInfo;
|
||||||
import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionTrigger;
|
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.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.AlarmAssignmentNotificationRuleTriggerConfig;
|
||||||
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig;
|
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig;
|
||||||
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig;
|
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.NewPlatformVersionNotificationRuleTriggerConfig;
|
||||||
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
|
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.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.NotificationTarget;
|
||||||
import org.thingsboard.server.common.data.notification.targets.platform.AffectedTenantAdministratorsFilter;
|
import org.thingsboard.server.common.data.notification.targets.platform.AffectedTenantAdministratorsFilter;
|
||||||
import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter;
|
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.rule.RuleChainMetaData;
|
||||||
import org.thingsboard.server.common.data.security.Authority;
|
import org.thingsboard.server.common.data.security.Authority;
|
||||||
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
|
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.DefaultNotifications;
|
||||||
import org.thingsboard.server.dao.notification.NotificationRequestService;
|
import org.thingsboard.server.dao.notification.NotificationRequestService;
|
||||||
import org.thingsboard.server.dao.rule.RuleChainService;
|
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.queue.notification.DefaultNotificationDeduplicationService;
|
||||||
import org.thingsboard.server.service.notification.rule.cache.DefaultNotificationRulesCache;
|
import org.thingsboard.server.service.notification.rule.cache.DefaultNotificationRulesCache;
|
||||||
import org.thingsboard.server.service.state.DeviceStateService;
|
import org.thingsboard.server.service.state.DeviceStateService;
|
||||||
|
import org.thingsboard.server.service.system.DefaultSystemInfoService;
|
||||||
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
|
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -134,6 +140,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
|
|||||||
@SpyBean
|
@SpyBean
|
||||||
private AlarmSubscriptionService alarmSubscriptionService;
|
private AlarmSubscriptionService alarmSubscriptionService;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
private DefaultSystemInfoService systemInfoService;
|
||||||
|
@Autowired
|
||||||
private NotificationRequestService notificationRequestService;
|
private NotificationRequestService notificationRequestService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private RateLimitService rateLimitService;
|
private RateLimitService rateLimitService;
|
||||||
@ -254,7 +262,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
|
|||||||
assertThat(info.getAlarmStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
|
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());
|
alarmSubscriptionService.acknowledgeAlarm(tenantId, alarm.getId(), System.currentTimeMillis());
|
||||||
AlarmStatus expectedStatus = AlarmStatus.ACTIVE_ACK;
|
AlarmStatus expectedStatus = AlarmStatus.ACTIVE_ACK;
|
||||||
AlarmSeverity expectedSeverity = AlarmSeverity.CRITICAL;
|
AlarmSeverity expectedSeverity = AlarmSeverity.CRITICAL;
|
||||||
@ -638,7 +646,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
|
|||||||
rule = saveNotificationRule(rule);
|
rule = saveNotificationRule(rule);
|
||||||
|
|
||||||
NotificationRuleInfo ruleInfo = findNotificationRules().getData().get(0);
|
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.getTemplateName()).isEqualTo(template.getName());
|
||||||
assertThat(ruleInfo.getDeliveryMethods()).containsOnly(deliveryMethods);
|
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
|
@Test
|
||||||
public void testNotificationRuleDisabling() throws Exception {
|
public void testNotificationRuleDisabling() throws Exception {
|
||||||
EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig();
|
EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig();
|
||||||
|
|||||||
@ -37,7 +37,8 @@ public enum NotificationType {
|
|||||||
RATE_LIMITS,
|
RATE_LIMITS,
|
||||||
EDGE_CONNECTION,
|
EDGE_CONNECTION,
|
||||||
EDGE_COMMUNICATION_FAILURE,
|
EDGE_COMMUNICATION_FAILURE,
|
||||||
TASK_PROCESSING_FAILURE;
|
TASK_PROCESSING_FAILURE,
|
||||||
|
RESOURCES_SHORTAGE;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private boolean system; // for future use and compatibility with PE
|
private boolean system; // for future use and compatibility with PE
|
||||||
|
|||||||
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -55,9 +55,4 @@ public class NewPlatformVersionTrigger implements NotificationRuleTrigger {
|
|||||||
updateInfo.getCurrentVersion(), updateInfo.getLatestVersion());
|
updateInfo.getCurrentVersion(), updateInfo.getLatestVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getDefaultDeduplicationDuration() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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.id.TenantId;
|
||||||
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
|
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
public class TaskProcessingFailureTrigger implements NotificationRuleTrigger {
|
public class TaskProcessingFailureTrigger implements NotificationRuleTrigger {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 5606203770553105345L;
|
||||||
|
|
||||||
private final HousekeeperTask task;
|
private final HousekeeperTask task;
|
||||||
private final int attempt;
|
private final int attempt;
|
||||||
private final Throwable error;
|
private final Throwable error;
|
||||||
@ -45,9 +50,4 @@ public class TaskProcessingFailureTrigger implements NotificationRuleTrigger {
|
|||||||
return task.getEntityId();
|
return task.getEntityId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean deduplicate() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,8 @@ import java.io.Serializable;
|
|||||||
@Type(value = RateLimitsNotificationRuleTriggerConfig.class, name = "RATE_LIMITS"),
|
@Type(value = RateLimitsNotificationRuleTriggerConfig.class, name = "RATE_LIMITS"),
|
||||||
@Type(value = EdgeConnectionNotificationRuleTriggerConfig.class, name = "EDGE_CONNECTION"),
|
@Type(value = EdgeConnectionNotificationRuleTriggerConfig.class, name = "EDGE_CONNECTION"),
|
||||||
@Type(value = EdgeCommunicationFailureNotificationRuleTriggerConfig.class, name = "EDGE_COMMUNICATION_FAILURE"),
|
@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 {
|
public interface NotificationRuleTriggerConfig extends Serializable {
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,8 @@ public enum NotificationRuleTriggerType {
|
|||||||
ENTITIES_LIMIT(false),
|
ENTITIES_LIMIT(false),
|
||||||
API_USAGE_LIMIT(false),
|
API_USAGE_LIMIT(false),
|
||||||
RATE_LIMITS(false),
|
RATE_LIMITS(false),
|
||||||
TASK_PROCESSING_FAILURE(false);
|
TASK_PROCESSING_FAILURE(false),
|
||||||
|
RESOURCES_SHORTAGE(false);
|
||||||
|
|
||||||
private final boolean tenantLevel;
|
private final boolean tenantLevel;
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -186,6 +186,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
|
|||||||
defaultNotifications.create(tenantId, DefaultNotifications.exceededRateLimitsForSysadmin, sysAdmins.getId());
|
defaultNotifications.create(tenantId, DefaultNotifications.exceededRateLimitsForSysadmin, sysAdmins.getId());
|
||||||
defaultNotifications.create(tenantId, DefaultNotifications.newPlatformVersion, sysAdmins.getId());
|
defaultNotifications.create(tenantId, DefaultNotifications.newPlatformVersion, sysAdmins.getId());
|
||||||
defaultNotifications.create(tenantId, DefaultNotifications.taskProcessingFailure, tenantAdmins.getId());
|
defaultNotifications.create(tenantId, DefaultNotifications.taskProcessingFailure, tenantAdmins.getId());
|
||||||
|
defaultNotifications.create(tenantId, DefaultNotifications.resourcesShortage, sysAdmins.getId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,6 +222,9 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
|
|||||||
if (!isNotificationConfigured(tenantId, NotificationType.TASK_PROCESSING_FAILURE)) {
|
if (!isNotificationConfigured(tenantId, NotificationType.TASK_PROCESSING_FAILURE)) {
|
||||||
defaultNotifications.create(tenantId, DefaultNotifications.taskProcessingFailure, sysAdmins.getId());
|
defaultNotifications.create(tenantId, DefaultNotifications.taskProcessingFailure, sysAdmins.getId());
|
||||||
}
|
}
|
||||||
|
if (!isNotificationConfigured(tenantId, NotificationType.RESOURCES_SHORTAGE)) {
|
||||||
|
defaultNotifications.create(tenantId, DefaultNotifications.resourcesShortage, sysAdmins.getId());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
var requiredNotificationTypes = List.of(NotificationType.EDGE_CONNECTION, NotificationType.EDGE_COMMUNICATION_FAILURE);
|
var requiredNotificationTypes = List.of(NotificationType.EDGE_CONNECTION, NotificationType.EDGE_COMMUNICATION_FAILURE);
|
||||||
var existingNotificationTypes = notificationTemplateService.findNotificationTemplatesByTenantIdAndNotificationTypes(
|
var existingNotificationTypes = notificationTemplateService.findNotificationTemplatesByTenantIdAndNotificationTypes(
|
||||||
|
|||||||
@ -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.EscalatedNotificationRuleRecipientsConfig;
|
||||||
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
|
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.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.AlarmAssignmentNotificationRuleTriggerConfig;
|
||||||
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig;
|
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig;
|
||||||
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig;
|
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.NotificationRuleTriggerConfig;
|
||||||
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
|
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.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.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig;
|
||||||
import org.thingsboard.server.common.data.notification.rule.trigger.config.TaskProcessingFailureNotificationRuleTriggerConfig;
|
import org.thingsboard.server.common.data.notification.rule.trigger.config.TaskProcessingFailureNotificationRuleTriggerConfig;
|
||||||
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
|
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
|
||||||
@ -372,6 +374,20 @@ public class DefaultNotifications {
|
|||||||
.build())
|
.build())
|
||||||
.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 NotificationTemplateService templateService;
|
||||||
private final NotificationRuleService ruleService;
|
private final NotificationRuleService ruleService;
|
||||||
|
|
||||||
|
|||||||
@ -480,17 +480,18 @@
|
|||||||
ignoreAuthorityFilter
|
ignoreAuthorityFilter
|
||||||
[allowedEntityTypes]="allowEntityTypeForEntitiesLimit">
|
[allowedEntityTypes]="allowEntityTypeForEntitiesLimit">
|
||||||
</tb-entity-type-list>
|
</tb-entity-type-list>
|
||||||
<div class="limit-slider-container flex flex-row items-center justify-start xs:flex-col xs:items-stretch">
|
<div class="limit-slider-container mb-4 flex flex-row items-center justify-start xs:flex-col xs:items-stretch">
|
||||||
<label translate>notification.threshold</label>
|
<span translate>notification.threshold</span>
|
||||||
<div class="flex flex-1 flex-row items-center justify-start">
|
<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">
|
<input matSliderThumb formControlName="threshold">
|
||||||
</mat-slider>
|
</mat-slider>
|
||||||
<mat-form-field class="limit-slider-value">
|
<mat-form-field class="limit-slider-value" subscriptSizing="dynamic">
|
||||||
<input matInput formControlName="threshold" type="number" step="0.01"
|
<input matInput formControlName="threshold" type="number" step="1"
|
||||||
[value]="entitiesLimitTemplateForm.get('triggerConfig.threshold').value"
|
[value]="entitiesLimitTemplateForm.get('triggerConfig.threshold').value"
|
||||||
min="0"
|
min="0"
|
||||||
max="1"/>
|
max="100"/>
|
||||||
|
<span class="mr-2" matSuffix>%</span>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -591,6 +592,69 @@
|
|||||||
</section>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
</mat-step>
|
</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>
|
</mat-horizontal-stepper>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|||||||
@ -81,7 +81,7 @@
|
|||||||
min-width: 364px;
|
min-width: 364px;
|
||||||
}
|
}
|
||||||
.limit-slider-container {
|
.limit-slider-container {
|
||||||
> label {
|
> span {
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
width: min-content;
|
width: min-content;
|
||||||
max-width: 40%;
|
max-width: 40%;
|
||||||
|
|||||||
@ -102,6 +102,7 @@ export class RuleNotificationDialogComponent extends
|
|||||||
edgeCommunicationFailureTemplateForm: FormGroup;
|
edgeCommunicationFailureTemplateForm: FormGroup;
|
||||||
edgeConnectionTemplateForm: FormGroup;
|
edgeConnectionTemplateForm: FormGroup;
|
||||||
taskProcessingFailureTemplateForm: FormGroup;
|
taskProcessingFailureTemplateForm: FormGroup;
|
||||||
|
resourceUsageShortageTemplateForm: FormGroup;
|
||||||
|
|
||||||
triggerType = TriggerType;
|
triggerType = TriggerType;
|
||||||
triggerTypes: TriggerType[];
|
triggerTypes: TriggerType[];
|
||||||
@ -315,7 +316,7 @@ export class RuleNotificationDialogComponent extends
|
|||||||
this.entitiesLimitTemplateForm = this.fb.group({
|
this.entitiesLimitTemplateForm = this.fb.group({
|
||||||
triggerConfig: this.fb.group({
|
triggerConfig: this.fb.group({
|
||||||
entityTypes: [],
|
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>([
|
this.triggerTypeFormsMap = new Map<TriggerType, FormGroup>([
|
||||||
[TriggerType.ALARM, this.alarmTemplateForm],
|
[TriggerType.ALARM, this.alarmTemplateForm],
|
||||||
[TriggerType.ALARM_COMMENT, this.alarmCommentTemplateForm],
|
[TriggerType.ALARM_COMMENT, this.alarmCommentTemplateForm],
|
||||||
@ -357,7 +366,8 @@ export class RuleNotificationDialogComponent extends
|
|||||||
[TriggerType.RATE_LIMITS, this.rateLimitsTemplateForm],
|
[TriggerType.RATE_LIMITS, this.rateLimitsTemplateForm],
|
||||||
[TriggerType.EDGE_COMMUNICATION_FAILURE, this.edgeCommunicationFailureTemplateForm],
|
[TriggerType.EDGE_COMMUNICATION_FAILURE, this.edgeCommunicationFailureTemplateForm],
|
||||||
[TriggerType.EDGE_CONNECTION, this.edgeConnectionTemplateForm],
|
[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) {
|
if (data.isAdd || data.isCopy) {
|
||||||
@ -380,6 +390,14 @@ export class RuleNotificationDialogComponent extends
|
|||||||
this.deviceInactivityTemplateForm.get('triggerConfig.filterByDevice')
|
this.deviceInactivityTemplateForm.get('triggerConfig.filterByDevice')
|
||||||
.patchValue(!!this.ruleNotification.triggerConfig.devices, {onlySelf: true});
|
.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) {
|
if (triggerType === TriggerType.DEVICE_ACTIVITY) {
|
||||||
delete formValue.triggerConfig.filterByDevice;
|
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.recipientsConfig.triggerType = triggerType;
|
||||||
formValue.triggerConfig.triggerType = triggerType;
|
formValue.triggerConfig.triggerType = triggerType;
|
||||||
if (this.ruleNotification && !this.data.isCopy) {
|
if (this.ruleNotification && !this.data.isCopy) {
|
||||||
@ -483,8 +509,7 @@ export class RuleNotificationDialogComponent extends
|
|||||||
}
|
}
|
||||||
|
|
||||||
formatLabel(value: number): string {
|
formatLabel(value: number): string {
|
||||||
const formatValue = (value * 100).toFixed();
|
return `${value}%`;
|
||||||
return `${formatValue}%`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private isSysAdmin(): boolean {
|
private isSysAdmin(): boolean {
|
||||||
@ -497,7 +522,8 @@ export class RuleNotificationDialogComponent extends
|
|||||||
TriggerType.API_USAGE_LIMIT,
|
TriggerType.API_USAGE_LIMIT,
|
||||||
TriggerType.NEW_PLATFORM_VERSION,
|
TriggerType.NEW_PLATFORM_VERSION,
|
||||||
TriggerType.RATE_LIMITS,
|
TriggerType.RATE_LIMITS,
|
||||||
TriggerType.TASK_PROCESSING_FAILURE
|
TriggerType.TASK_PROCESSING_FAILURE,
|
||||||
|
TriggerType.RESOURCES_SHORTAGE
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (this.isSysAdmin()) {
|
if (this.isSysAdmin()) {
|
||||||
|
|||||||
@ -180,7 +180,8 @@ export class TemplateNotificationDialogComponent
|
|||||||
NotificationType.API_USAGE_LIMIT,
|
NotificationType.API_USAGE_LIMIT,
|
||||||
NotificationType.NEW_PLATFORM_VERSION,
|
NotificationType.NEW_PLATFORM_VERSION,
|
||||||
NotificationType.RATE_LIMITS,
|
NotificationType.RATE_LIMITS,
|
||||||
NotificationType.TASK_PROCESSING_FAILURE
|
NotificationType.TASK_PROCESSING_FAILURE,
|
||||||
|
NotificationType.RESOURCES_SHORTAGE
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (this.isSysAdmin()) {
|
if (this.isSysAdmin()) {
|
||||||
|
|||||||
@ -129,7 +129,7 @@ export interface NotificationRule extends Omit<BaseData<NotificationRuleId>, 'la
|
|||||||
export type NotificationRuleTriggerConfig = Partial<AlarmNotificationRuleTriggerConfig & DeviceInactivityNotificationRuleTriggerConfig &
|
export type NotificationRuleTriggerConfig = Partial<AlarmNotificationRuleTriggerConfig & DeviceInactivityNotificationRuleTriggerConfig &
|
||||||
EntityActionNotificationRuleTriggerConfig & AlarmCommentNotificationRuleTriggerConfig & AlarmAssignmentNotificationRuleTriggerConfig &
|
EntityActionNotificationRuleTriggerConfig & AlarmCommentNotificationRuleTriggerConfig & AlarmAssignmentNotificationRuleTriggerConfig &
|
||||||
RuleEngineLifecycleEventNotificationRuleTriggerConfig & EntitiesLimitNotificationRuleTriggerConfig &
|
RuleEngineLifecycleEventNotificationRuleTriggerConfig & EntitiesLimitNotificationRuleTriggerConfig &
|
||||||
ApiUsageLimitNotificationRuleTriggerConfig & RateLimitsNotificationRuleTriggerConfig>;
|
ApiUsageLimitNotificationRuleTriggerConfig & RateLimitsNotificationRuleTriggerConfig & ResourceUsageShortageNotificationRuleTriggerConfig>;
|
||||||
|
|
||||||
export interface AlarmNotificationRuleTriggerConfig {
|
export interface AlarmNotificationRuleTriggerConfig {
|
||||||
alarmTypes?: Array<string>;
|
alarmTypes?: Array<string>;
|
||||||
@ -183,6 +183,12 @@ export interface EntitiesLimitNotificationRuleTriggerConfig {
|
|||||||
threshold: number;
|
threshold: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ResourceUsageShortageNotificationRuleTriggerConfig {
|
||||||
|
cpuThreshold: number;
|
||||||
|
ramThreshold: number;
|
||||||
|
storageThreshold: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApiUsageLimitNotificationRuleTriggerConfig {
|
export interface ApiUsageLimitNotificationRuleTriggerConfig {
|
||||||
apiFeatures: ApiFeature[];
|
apiFeatures: ApiFeature[];
|
||||||
notifyOn: ApiUsageStateValue[];
|
notifyOn: ApiUsageStateValue[];
|
||||||
@ -526,7 +532,8 @@ export enum NotificationType {
|
|||||||
RATE_LIMITS = 'RATE_LIMITS',
|
RATE_LIMITS = 'RATE_LIMITS',
|
||||||
EDGE_CONNECTION = 'EDGE_CONNECTION',
|
EDGE_CONNECTION = 'EDGE_CONNECTION',
|
||||||
EDGE_COMMUNICATION_FAILURE = 'EDGE_COMMUNICATION_FAILURE',
|
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>([
|
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.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, 'settings_ethernet'],
|
||||||
[NotificationType.ENTITIES_LIMIT, 'data_thresholding'],
|
[NotificationType.ENTITIES_LIMIT, 'data_thresholding'],
|
||||||
[NotificationType.API_USAGE_LIMIT, 'insert_chart'],
|
[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>(
|
export const AlarmSeverityNotificationColors = new Map<AlarmSeverity, string>(
|
||||||
@ -657,6 +665,12 @@ export const NotificationTemplateTypeTranslateMap = new Map<NotificationType, No
|
|||||||
helpId: 'notification/task_processing_failure'
|
helpId: 'notification/task_processing_failure'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
[NotificationType.RESOURCES_SHORTAGE,
|
||||||
|
{
|
||||||
|
name: 'notification.template-type.resources-shortage',
|
||||||
|
helpId: 'notification/resources_shortage'
|
||||||
|
}
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export enum TriggerType {
|
export enum TriggerType {
|
||||||
@ -673,6 +687,7 @@ export enum TriggerType {
|
|||||||
EDGE_CONNECTION = 'EDGE_CONNECTION',
|
EDGE_CONNECTION = 'EDGE_CONNECTION',
|
||||||
EDGE_COMMUNICATION_FAILURE = 'EDGE_COMMUNICATION_FAILURE',
|
EDGE_COMMUNICATION_FAILURE = 'EDGE_COMMUNICATION_FAILURE',
|
||||||
TASK_PROCESSING_FAILURE = 'TASK_PROCESSING_FAILURE',
|
TASK_PROCESSING_FAILURE = 'TASK_PROCESSING_FAILURE',
|
||||||
|
RESOURCES_SHORTAGE = 'RESOURCES_SHORTAGE'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TriggerTypeTranslationMap = new Map<TriggerType, string>([
|
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.RATE_LIMITS, 'notification.trigger.rate-limits'],
|
||||||
[TriggerType.EDGE_CONNECTION, 'notification.trigger.edge-connection'],
|
[TriggerType.EDGE_CONNECTION, 'notification.trigger.edge-connection'],
|
||||||
[TriggerType.EDGE_COMMUNICATION_FAILURE, 'notification.trigger.edge-communication-failure'],
|
[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 {
|
export interface NotificationUserSettings {
|
||||||
|
|||||||
@ -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>
|
||||||
@ -3876,6 +3876,7 @@
|
|||||||
"new-platform-version-trigger-settings": "New platform version trigger settings",
|
"new-platform-version-trigger-settings": "New platform version trigger settings",
|
||||||
"rate-limits-trigger-settings": "Exceeded rate limits trigger settings",
|
"rate-limits-trigger-settings": "Exceeded rate limits trigger settings",
|
||||||
"task-processing-failure-trigger-settings": "Task processing failure 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",
|
"at-least-one-should-be-selected": "At least one should be selected",
|
||||||
"basic-settings": "Basic settings",
|
"basic-settings": "Basic settings",
|
||||||
"button-text": "Button text",
|
"button-text": "Button text",
|
||||||
@ -3890,6 +3891,7 @@
|
|||||||
"create-new": "Create new",
|
"create-new": "Create new",
|
||||||
"created": "Created",
|
"created": "Created",
|
||||||
"customize-messages": "Customize messages",
|
"customize-messages": "Customize messages",
|
||||||
|
"cpu-threshold": "CPU threshold",
|
||||||
"delete-notification-text": "Be careful, after the confirmation the notification will become unrecoverable.",
|
"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-notification-title": "Are you sure you want to delete the notification?",
|
||||||
"delete-notifications-text": "Be careful, after the confirmation notifications will become unrecoverable.",
|
"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-chain-lifecycle-failures": "Only rule chain lifecycle failures",
|
||||||
"only-rule-node-lifecycle-failures": "Only rule node lifecycle failures",
|
"only-rule-node-lifecycle-failures": "Only rule node lifecycle failures",
|
||||||
"platform-users": "Platform users",
|
"platform-users": "Platform users",
|
||||||
|
"ram-threshold": "RAM threshold",
|
||||||
"rate-limits": "Rate limits",
|
"rate-limits": "Rate limits",
|
||||||
"rate-limits-hint": "If the field is empty, the trigger will be applied to all rate limits",
|
"rate-limits-hint": "If the field is empty, the trigger will be applied to all rate limits",
|
||||||
"recipient": "Recipient",
|
"recipient": "Recipient",
|
||||||
@ -4070,6 +4073,7 @@
|
|||||||
"start-from-scratch": "Start from scratch",
|
"start-from-scratch": "Start from scratch",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"stop-escalation-alarm-status-become": "Stop the escalation on the alarm status become:",
|
"stop-escalation-alarm-status-become": "Stop the escalation on the alarm status become:",
|
||||||
|
"storage-threshold": "Storage threshold",
|
||||||
"subject": "Subject",
|
"subject": "Subject",
|
||||||
"subject-required": "Subject is required",
|
"subject-required": "Subject is required",
|
||||||
"subject-max-length": "Subject should be less than or equal to {{ length }} characters",
|
"subject-max-length": "Subject should be less than or equal to {{ length }} characters",
|
||||||
@ -4091,7 +4095,8 @@
|
|||||||
"rate-limits": "Exceeded rate limits",
|
"rate-limits": "Exceeded rate limits",
|
||||||
"edge-communication-failure": "Edge communication failure",
|
"edge-communication-failure": "Edge communication failure",
|
||||||
"edge-connection": "Edge connection",
|
"edge-connection": "Edge connection",
|
||||||
"task-processing-failure": "Task processing failure"
|
"task-processing-failure": "Task processing failure",
|
||||||
|
"resources-shortage": "Resources shortage"
|
||||||
},
|
},
|
||||||
"templates": "Templates",
|
"templates": "Templates",
|
||||||
"notification-templates": "Notifications / Templates",
|
"notification-templates": "Notifications / Templates",
|
||||||
@ -4115,6 +4120,7 @@
|
|||||||
"edge-connection": "Edge connection",
|
"edge-connection": "Edge connection",
|
||||||
"edge-communication-failure": "Edge communication failure",
|
"edge-communication-failure": "Edge communication failure",
|
||||||
"task-processing-failure": "Task processing failure",
|
"task-processing-failure": "Task processing failure",
|
||||||
|
"resources-shortage": "Resources shortage",
|
||||||
"trigger": "Trigger",
|
"trigger": "Trigger",
|
||||||
"trigger-required": "Trigger is required"
|
"trigger-required": "Trigger is required"
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user