This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
The following commit(s) were added to refs/heads/master by this push:
new f64da5fcf7 [feature][alerter] Support notification template preview
(#4338)
f64da5fcf7 is described below
commit f64da5fcf70c969a333268f0273964cb06bb49d0
Author: Prabal Pratap Singh <[email protected]>
AuthorDate: Sat Aug 22 18:15:02 2026 +0530
[feature][alerter] Support notification template preview (#4338)
---
.../alert/controller/NoticeConfigController.java | 20 ++++
...andlerImpl.java => NoticeTemplateRenderer.java} | 103 ++++++++-------------
.../impl/AbstractAlertNotifyHandlerImpl.java | 32 +------
.../controller/NoticeConfigControllerTest.java | 42 +++++++++
4 files changed, 106 insertions(+), 91 deletions(-)
diff --git
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/NoticeConfigController.java
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/NoticeConfigController.java
index 151fc0b12f..2b84e9bed2 100644
---
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/NoticeConfigController.java
+++
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/NoticeConfigController.java
@@ -29,8 +29,11 @@ import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeRule;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
+import org.apache.hertzbeat.alert.AlerterProperties;
+import org.apache.hertzbeat.alert.notice.NoticeTemplateRenderer;
import org.apache.hertzbeat.alert.service.NoticeConfigService;
import org.apache.hertzbeat.alert.util.NoticeReceiverMaskUtil;
+import org.apache.hertzbeat.common.util.ResourceBundleUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
@@ -55,6 +58,9 @@ public class NoticeConfigController {
@Autowired
private NoticeConfigService noticeConfigService;
+ @Autowired
+ private AlerterProperties alerterProperties;
+
@PostMapping(path = "/receiver")
@Operation(summary = "Add a recipient", description = "Add a recipient")
public ResponseEntity<Message<Void>> addNewNoticeReceiver(@Valid
@RequestBody NoticeReceiver noticeReceiver) {
@@ -228,4 +234,18 @@ public class NoticeConfigController {
}
return ResponseEntity.ok(Message.fail(FAIL_CODE, "Notify service not
available, please check config!"));
}
+
+ @PostMapping(path = "/template/preview")
+ @Operation(summary = "Preview how a notice template renders against a
sample alert",
+ description = "Preview how a notice template renders against a
sample alert, without sending anything")
+ public ResponseEntity<Message<String>> previewNoticeTemplate(@Valid
@RequestBody NoticeTemplate noticeTemplate) {
+ try {
+ String rendered = NoticeTemplateRenderer.renderContent(
+ noticeTemplate, NoticeTemplateRenderer.sampleGroupAlert(),
alerterProperties.getConsoleUrl(),
+ ResourceBundleUtil.getBundle("alerter"));
+ return ResponseEntity.ok(Message.successWithData(rendered));
+ } catch (Exception e) {
+ return ResponseEntity.ok(Message.fail(FAIL_CODE, "Failed to render
template: " + e.getMessage()));
+ }
+ }
}
diff --git
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/AbstractAlertNotifyHandlerImpl.java
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/NoticeTemplateRenderer.java
similarity index 50%
copy from
hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/AbstractAlertNotifyHandlerImpl.java
copy to
hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/NoticeTemplateRenderer.java
index a37b9a2684..6630e54108 100644
---
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/AbstractAlertNotifyHandlerImpl.java
+++
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/NoticeTemplateRenderer.java
@@ -15,46 +15,41 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.alert.notice.impl;
+package org.apache.hertzbeat.alert.notice;
import freemarker.cache.StringTemplateLoader;
import freemarker.core.TemplateClassResolver;
import freemarker.template.Configuration;
import freemarker.template.TemplateException;
import java.io.IOException;
+import java.time.LocalDateTime;
import java.util.HashMap;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.hertzbeat.alert.AlerterProperties;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
-import org.apache.hertzbeat.common.support.event.SystemConfigChangeEvent;
-import org.apache.hertzbeat.common.util.ResourceBundleUtil;
-import org.apache.hertzbeat.alert.notice.AlertNotifyHandler;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.event.EventListener;
+import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
-import org.springframework.web.client.RestTemplate;
/**
- * Abstract alert notify handler implementation.
+ * Renders a {@link NoticeTemplate} against a {@link GroupAlert} using
FreeMarker.
+ *
+ * <p>This is shared between the real notify dispatch path ({@code
AlertNotifyHandler}
+ * implementations) and the notice template preview endpoint, so both render a
template
+ * exactly the same way.
*/
-@Slf4j
-abstract class AbstractAlertNotifyHandlerImpl implements AlertNotifyHandler {
+public final class NoticeTemplateRenderer {
private static final String NUMBER_FORMAT = "0";
- protected ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter");
- @Autowired
- protected RestTemplate restTemplate;
- @Autowired
- protected AlerterProperties alerterProperties;
+ private NoticeTemplateRenderer() {
+ }
- protected String renderContent(NoticeTemplate noticeTemplate, GroupAlert
alert) throws TemplateException, IOException {
+ public static String renderContent(NoticeTemplate noticeTemplate,
GroupAlert alert, String consoleUrl,
+ ResourceBundle bundle) throws TemplateException, IOException {
StringTemplateLoader stringLoader = new StringTemplateLoader();
- freemarker.template.Template templateRes;
Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
cfg.setNumberFormat(NUMBER_FORMAT);
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
@@ -65,57 +60,41 @@ abstract class AbstractAlertNotifyHandlerImpl implements
AlertNotifyHandler {
model.put("commonLabels", alert.getCommonLabels());
model.put("commonAnnotations", alert.getCommonAnnotations());
model.put("alerts", alert.getAlerts());
- if (alerterProperties != null) {
- model.put("consoleUrl", alerterProperties.getConsoleUrl());
+ if (consoleUrl != null) {
+ model.put("consoleUrl", consoleUrl);
}
// TODO Single instance reuse cache considers multiple-threading issues
String templateName = "freeMakerTemplate";
stringLoader.putTemplate(templateName, noticeTemplate.getContent());
cfg.setTemplateLoader(stringLoader);
- templateRes = cfg.getTemplate(templateName, Locale.CHINESE);
+ freemarker.template.Template templateRes =
cfg.getTemplate(templateName, Locale.CHINESE);
String template =
FreeMarkerTemplateUtils.processTemplateIntoString(templateRes, model);
return template.replaceAll("((\r\n)|\n)[\\s\t ]*(\\1)+", "$1");
}
- protected String escapeJsonStr(String jsonStr){
- if (jsonStr == null) {
- return null;
- }
-
- StringBuilder sb = new StringBuilder();
- for (char c : jsonStr.toCharArray()) {
- switch (c) {
- case '"':
- sb.append("\\\"");
- break;
- case '\\':
- sb.append("\\\\");
- break;
- case '\b':
- sb.append("\\b");
- break;
- case '\f':
- sb.append("\\f");
- break;
- case '\n':
- sb.append("\\n");
- break;
- case '\r':
- sb.append("\\r");
- break;
- case '\t':
- sb.append("\\t");
- break;
- default:
- sb.append(c);
- }
- }
- return sb.toString();
- }
-
- @EventListener(SystemConfigChangeEvent.class)
- public void onEvent(SystemConfigChangeEvent event) {
- log.info("{} receive system config change event: {}.",
this.getClass().getName(), event.getSource());
- this.bundle = ResourceBundleUtil.getBundle("alerter");
+ /**
+ * Builds a representative {@link GroupAlert} for previewing a template,
so a user can see
+ * what a real notification would look like without waiting for (or
faking) a real alert.
+ */
+ public static GroupAlert sampleGroupAlert() {
+ long now = System.currentTimeMillis();
+ SingleAlert singleAlert = SingleAlert.builder()
+ .labels(Map.of("alertname", "HighCPUUsage", "instance",
"server1.example.com", "severity", "critical"))
+ .annotations(Map.of("summary", "High CPU usage detected"))
+ .content("CPU usage is above 80% for the last 5 minutes on
instance server1.example.com.")
+ .status("firing")
+ .triggerTimes(1)
+ .startAt(now)
+ .activeAt(now)
+ .build();
+ return GroupAlert.builder()
+ .groupKey("HighCPUUsage{alertname=\"HighCPUUsage\",
instance=\"server1.example.com\"}")
+ .status("firing")
+ .groupLabels(Map.of("alertname", "HighCPUUsage"))
+ .commonLabels(Map.of("alertname", "HighCPUUsage", "instance",
"server1.example.com", "severity", "critical"))
+ .commonAnnotations(Map.of("summary", "High CPU usage
detected"))
+ .gmtCreate(LocalDateTime.now())
+ .alerts(List.of(singleAlert))
+ .build();
}
}
diff --git
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/AbstractAlertNotifyHandlerImpl.java
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/AbstractAlertNotifyHandlerImpl.java
index a37b9a2684..640f84abc9 100644
---
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/AbstractAlertNotifyHandlerImpl.java
+++
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/AbstractAlertNotifyHandlerImpl.java
@@ -17,14 +17,8 @@
package org.apache.hertzbeat.alert.notice.impl;
-import freemarker.cache.StringTemplateLoader;
-import freemarker.core.TemplateClassResolver;
-import freemarker.template.Configuration;
import freemarker.template.TemplateException;
import java.io.IOException;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
import java.util.ResourceBundle;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.AlerterProperties;
@@ -33,9 +27,9 @@ import
org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.common.support.event.SystemConfigChangeEvent;
import org.apache.hertzbeat.common.util.ResourceBundleUtil;
import org.apache.hertzbeat.alert.notice.AlertNotifyHandler;
+import org.apache.hertzbeat.alert.notice.NoticeTemplateRenderer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
-import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.springframework.web.client.RestTemplate;
/**
@@ -53,28 +47,8 @@ abstract class AbstractAlertNotifyHandlerImpl implements
AlertNotifyHandler {
protected String renderContent(NoticeTemplate noticeTemplate, GroupAlert
alert) throws TemplateException, IOException {
- StringTemplateLoader stringLoader = new StringTemplateLoader();
- freemarker.template.Template templateRes;
- Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
- cfg.setNumberFormat(NUMBER_FORMAT);
- cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
- Map<String, Object> model = new HashMap<>(16);
- model.put("title", bundle.getString("alerter.notify.title"));
- model.put("status", alert.getStatus());
- model.put("groupLabels", alert.getGroupLabels());
- model.put("commonLabels", alert.getCommonLabels());
- model.put("commonAnnotations", alert.getCommonAnnotations());
- model.put("alerts", alert.getAlerts());
- if (alerterProperties != null) {
- model.put("consoleUrl", alerterProperties.getConsoleUrl());
- }
- // TODO Single instance reuse cache considers multiple-threading issues
- String templateName = "freeMakerTemplate";
- stringLoader.putTemplate(templateName, noticeTemplate.getContent());
- cfg.setTemplateLoader(stringLoader);
- templateRes = cfg.getTemplate(templateName, Locale.CHINESE);
- String template =
FreeMarkerTemplateUtils.processTemplateIntoString(templateRes, model);
- return template.replaceAll("((\r\n)|\n)[\\s\t ]*(\\1)+", "$1");
+ String consoleUrl = alerterProperties != null ?
alerterProperties.getConsoleUrl() : null;
+ return NoticeTemplateRenderer.renderContent(noticeTemplate, alert,
consoleUrl, bundle);
}
protected String escapeJsonStr(String jsonStr){
diff --git
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/NoticeConfigControllerTest.java
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/NoticeConfigControllerTest.java
index f82556c822..32ab567dda 100644
---
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/NoticeConfigControllerTest.java
+++
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/NoticeConfigControllerTest.java
@@ -30,6 +30,7 @@ import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
+import org.apache.hertzbeat.alert.AlerterProperties;
import org.apache.hertzbeat.alert.service.impl.NoticeConfigServiceImpl;
import org.apache.hertzbeat.alert.util.NoticeReceiverMaskUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
@@ -64,6 +65,9 @@ class NoticeConfigControllerTest {
@Mock
private NoticeConfigServiceImpl noticeConfigService;
+ @Mock
+ private AlerterProperties alerterProperties;
+
@InjectMocks
private NoticeConfigController noticeConfigController;
@@ -510,4 +514,42 @@ class NoticeConfigControllerTest {
.andExpect(jsonPath("$.code").value((int)
CommonConstants.SUCCESS_CODE))
.andReturn();
}
+
+ @Test
+ void previewNoticeTemplate() throws Exception {
+ NoticeTemplate noticeTemplate = new NoticeTemplate();
+ noticeTemplate.setId(5L);
+ noticeTemplate.setName("preview-test");
+ noticeTemplate.setType((byte) 5);
+ noticeTemplate.setContent("""
+ [${title}] status=${status}
+ <#list alerts as alert>
+ ${alert.labels.alertname} - ${alert.content}
+ </#list>""");
+
when(alerterProperties.getConsoleUrl()).thenReturn("http://localhost:1157");
+
+ this.mockMvc.perform(post("/api/notice/template/preview")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.toJson(noticeTemplate)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value((int)
CommonConstants.SUCCESS_CODE))
+
.andExpect(jsonPath("$.data").value(org.hamcrest.Matchers.containsString("HighCPUUsage")))
+ .andReturn();
+ }
+
+ @Test
+ void previewNoticeTemplateWithInvalidContent() throws Exception {
+ NoticeTemplate noticeTemplate = new NoticeTemplate();
+ noticeTemplate.setId(5L);
+ noticeTemplate.setName("preview-test-invalid");
+ noticeTemplate.setType((byte) 5);
+ noticeTemplate.setContent("${undefinedVariable}");
+
+ this.mockMvc.perform(post("/api/notice/template/preview")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.toJson(noticeTemplate)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value((int)
CommonConstants.FAIL_CODE))
+ .andReturn();
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]