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]

Reply via email to