diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java index 1cb94bd53b..def1d13029 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java @@ -20,7 +20,6 @@ import org.thingsboard.server.actors.TbActor; import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorId; import org.thingsboard.server.actors.TbEntityActorId; -import org.thingsboard.server.actors.service.ComponentActor; import org.thingsboard.server.actors.service.ContextBasedCreator; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; @@ -30,7 +29,7 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; -public class RuleChainActor extends ComponentActor { +public class RuleChainActor extends RuleEngineComponentActor { private final RuleChain ruleChain; @@ -106,6 +105,11 @@ public class RuleChainActor extends ComponentActor> extends ComponentActor { + + public RuleEngineComponentActor(ActorSystemContext systemContext, TenantId tenantId, T id) { + super(systemContext, tenantId, id); + } + + @Override + protected void logLifecycleEvent(ComponentLifecycleEvent event, Exception e) { + super.logLifecycleEvent(event, e); + systemContext.getNotificationRuleProcessingService().process(tenantId, getRuleChainId(), getRuleChainName(), + id, processor.getComponentName(), event, e); + } + + protected abstract RuleChainId getRuleChainId(); + + protected abstract String getRuleChainName(); + +} diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java index 5115e30d7e..1af550a53b 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java @@ -21,7 +21,6 @@ import org.thingsboard.server.actors.TbActor; import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorId; import org.thingsboard.server.actors.TbEntityActorId; -import org.thingsboard.server.actors.service.ComponentActor; import org.thingsboard.server.actors.service.ContextBasedCreator; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; @@ -32,7 +31,7 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; @Slf4j -public class RuleNodeActor extends ComponentActor { +public class RuleNodeActor extends RuleEngineComponentActor { private final String ruleChainName; private final RuleChainId ruleChainId; @@ -138,6 +137,11 @@ public class RuleNodeActor extends ComponentActor lengthLimit) { + errorMsg = StringUtils.substring(errorMsg, 0, lengthLimit + 1).trim() + "[...]"; + } + return errorMsg; + } + @Override public NotificationRuleTriggerType getTriggerType() { return NotificationRuleTriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT; @@ -88,6 +100,7 @@ public class RuleEngineComponentLifecycleEventTriggerProcessor implements Notifi @Builder public static class RuleEngineComponentLifecycleEventTriggerObject { private final RuleChainId ruleChainId; + private final String ruleChainName; private final EntityId componentId; private final String componentName; private final ComponentLifecycleEvent eventType; diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java index b61d87a0af..6c3aa1e2f0 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java @@ -24,6 +24,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.data.util.Pair; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.debug.TbMsgGeneratorNode; +import org.thingsboard.rule.engine.debug.TbMsgGeneratorNodeConfiguration; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -40,6 +42,7 @@ import org.thingsboard.server.common.data.device.profile.AlarmRule; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; import org.thingsboard.server.common.data.id.NotificationRuleId; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationRequest; @@ -53,13 +56,19 @@ import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.notification.NotificationRequestService; @@ -130,7 +139,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { getWsClient().waitForUpdate(true); Notification notification = getWsClient().getLastDataUpdate().getUpdate(); - assertThat(notification.getSubject()).isEqualTo("ADDED: DEVICE [" + device.getId() + "]"); + assertThat(notification.getSubject()).isEqualTo("added: DEVICE [" + device.getId() + "]"); assertThat(notification.getText()).isEqualTo("User: " + TENANT_ADMIN_EMAIL); @@ -140,7 +149,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { getWsClient().waitForUpdate(true); notification = getWsClient().getLastDataUpdate().getUpdate(); - assertThat(notification.getSubject()).isEqualTo("UPDATED: DEVICE [" + device.getId() + "]"); + assertThat(notification.getSubject()).isEqualTo("updated: DEVICE [" + device.getId() + "]"); getWsClient().registerWaitForUpdate(); @@ -148,7 +157,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { getWsClient().waitForUpdate(true); notification = getWsClient().getLastDataUpdate().getUpdate(); - assertThat(notification.getSubject()).isEqualTo("DELETED: DEVICE [" + device.getId() + "]"); + assertThat(notification.getSubject()).isEqualTo("deleted: DEVICE [" + device.getId() + "]"); } @Test @@ -307,6 +316,48 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { assertThat(findNotificationRequests(EntityType.ALARM).getData()).filteredOn(NotificationRequest::isScheduled).isEmpty(); } + @Test + public void testNotificationRuleProcessing_ruleEngineComponentLifecycleEvent_ruleNodeStartError() { + String subject = "Rule Node '${componentName}' in Rule Chain '${ruleChainName}' failed to start"; + String text = "The error: ${error}"; + NotificationTemplate template = createNotificationTemplate(NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, subject, text, NotificationDeliveryMethod.PUSH); + + NotificationRule rule = new NotificationRule(); + rule.setName("Rule node start-up failures in my rule chain"); + rule.setTemplateId(template.getId()); + rule.setTriggerType(NotificationRuleTriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT); + + RuleChain ruleChain = createEmptyRuleChain("My Rule Chain"); + var triggerConfig = new RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig(); + triggerConfig.setRuleChains(Set.of(ruleChain.getUuidId())); + triggerConfig.setRuleChainEvents(Set.of(ComponentLifecycleEvent.STARTED)); + triggerConfig.setOnlyRuleChainLifecycleFailures(true); + + triggerConfig.setTrackRuleNodeEvents(true); + triggerConfig.setRuleNodeEvents(Set.of(ComponentLifecycleEvent.STARTED)); + triggerConfig.setOnlyRuleNodeLifecycleFailures(true); + rule.setTriggerConfig(triggerConfig); + + NotificationTarget target = createNotificationTarget(tenantAdminUserId); + DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); + recipientsConfig.setTriggerType(NotificationRuleTriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT); + recipientsConfig.setTargets(List.of(target.getUuidId())); + rule.setRecipientsConfig(recipientsConfig); + rule = saveNotificationRule(rule); + + getWsClient().subscribeForUnreadNotifications(10).waitForReply(true); + getWsClient().registerWaitForUpdate(); + + addRuleNodeWithError(ruleChain.getId(), "My generator"); + + getWsClient().waitForUpdate(10000, true); + Notification notification = getWsClient().getLastDataUpdate().getUpdate(); + + assertThat(notification.getType()).isEqualTo(NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT); + assertThat(notification.getSubject()).isEqualTo("Rule Node 'My generator' in Rule Chain 'My Rule Chain' failed to start"); + assertThat(notification.getText()).startsWith("The error: Can't compile script"); + } + @Test public void testNotificationRuleInfo() throws Exception { NotificationDeliveryMethod[] deliveryMethods = {NotificationDeliveryMethod.PUSH, NotificationDeliveryMethod.EMAIL}; @@ -368,6 +419,41 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { return deviceProfile; } + private RuleChain createEmptyRuleChain(String name) { + RuleChain ruleChain = new RuleChain(); + ruleChain.setName(name); + ruleChain.setTenantId(tenantId); + ruleChain.setRoot(false); + ruleChain.setDebugMode(false); + ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); + + RuleChainMetaData metaData = new RuleChainMetaData(); + metaData.setRuleChainId(ruleChain.getId()); + metaData.setNodes(List.of()); + metaData = doPost("/api/ruleChain/metadata", metaData, RuleChainMetaData.class); + return ruleChain; + } + + private RuleNode addRuleNodeWithError(RuleChainId ruleChainId, String name) { + RuleChainMetaData metaData = new RuleChainMetaData(); + metaData.setRuleChainId(ruleChainId); + + RuleNode generatorNodeWithError = new RuleNode(); + generatorNodeWithError.setName(name); + generatorNodeWithError.setType(TbMsgGeneratorNode.class.getName()); + TbMsgGeneratorNodeConfiguration generatorNodeConfiguration = new TbMsgGeneratorNodeConfiguration(); + generatorNodeConfiguration.setScriptLang(ScriptLanguage.JS); + generatorNodeConfiguration.setPeriodInSeconds(1000); + generatorNodeConfiguration.setMsgCount(1); + generatorNodeConfiguration.setJsScript("[return"); + generatorNodeWithError.setConfiguration(mapper.valueToTree(generatorNodeConfiguration)); + + metaData.setNodes(List.of(generatorNodeWithError)); + metaData.setFirstNodeIndex(0); + metaData = doPost("/api/ruleChain/metadata", metaData, RuleChainMetaData.class); + return metaData.getNodes().get(0); + } + private NotificationRule saveNotificationRule(NotificationRule notificationRule) { return doPost("/api/notification/rule", notificationRule, NotificationRule.class); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationProcessingContext.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationProcessingContext.java index 4d16762d85..2911ca2a69 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationProcessingContext.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationProcessingContext.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.notification.template.NotificationTemp import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; import org.thingsboard.server.common.data.notification.template.PushDeliveryMethodNotificationTemplate; +import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.Map; @@ -94,9 +95,10 @@ public class NotificationProcessingContext { } public T getProcessedTemplate(NotificationDeliveryMethod deliveryMethod, Map templateContext) { - if (request.getInfo() != null && deliveryMethod != NotificationDeliveryMethod.PUSH) { // for push notifications we are processing template from info on each serialization + NotificationInfo info = request.getInfo(); + if (info != null && deliveryMethod != NotificationDeliveryMethod.PUSH) { // for push notifications we are processing template from info on each serialization templateContext = new HashMap<>(templateContext); - templateContext.putAll(request.getInfo().getTemplateData()); + templateContext.putAll(info.getTemplateData()); } T template = (T) templates.get(deliveryMethod).copy(); @@ -114,7 +116,7 @@ public class NotificationProcessingContext { if (buttonConfig.isPresent()) { JsonNode link = buttonConfig.get().get("link"); if (link != null && link.isTextual()) { - link = new TextNode(processTemplate(link.asText(), templateContext, request.getInfo().getTemplateData())); + link = new TextNode(processTemplate(link.asText(), templateContext, info != null ? info.getTemplateData() : Collections.emptyMap())); buttonConfig.get().set("link", link); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/RuleEngineComponentLifecycleEventNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/RuleEngineComponentLifecycleEventNotificationInfo.java index 928aad6288..d2006c7098 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/RuleEngineComponentLifecycleEventNotificationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/RuleEngineComponentLifecycleEventNotificationInfo.java @@ -20,6 +20,7 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; @@ -33,21 +34,22 @@ import java.util.Map; public class RuleEngineComponentLifecycleEventNotificationInfo implements NotificationInfo { private RuleChainId ruleChainId; + private String ruleChainName; private EntityId componentId; private String componentName; private ComponentLifecycleEvent eventType; private String error; - // TODO: add rule chain name @Override public Map getTemplateData() { return Map.of( "ruleChainId", ruleChainId.toString(), + "ruleChainName", ruleChainName, "componentId", componentId.toString(), "componentType", componentId.getEntityType().name(), "componentName", componentName, "eventType", eventType.name(), - "error", Strings.nullToEmpty(error) + "error", error ); }