This is an automated email from the ASF dual-hosted git repository.

zqr10159 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 b09616e65c [bugfix] fix YAML monitor import and export round trips 
(#4404)
b09616e65c is described below

commit b09616e65c4b3db0acea33be2151ec1c5e4e395d
Author: Liming Deng <[email protected]>
AuthorDate: Wed Sep 30 16:58:22 2026 +0800

    [bugfix] fix YAML monitor import and export round trips (#4404)
    
    Co-authored-by: NekoPunch 
<[email protected]>
    Co-authored-by: Logic <[email protected]>
---
 .../service/impl/YamlImExportServiceImpl.java      |  49 ++++-
 .../manager/service/YamlImExportServiceTest.java   | 209 +++++++++++++++------
 2 files changed, 196 insertions(+), 62 deletions(-)

diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/YamlImExportServiceImpl.java
 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/YamlImExportServiceImpl.java
index f43983b69b..c5187f24fd 100644
--- 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/YamlImExportServiceImpl.java
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/YamlImExportServiceImpl.java
@@ -22,17 +22,27 @@ import static 
org.apache.hertzbeat.common.constants.ExportFileConstants.YamlFile
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.common.util.JsonUtil;
 import org.springframework.stereotype.Service;
 import org.apache.hertzbeat.common.util.export.YamlExportUtils;
+import org.yaml.snakeyaml.LoaderOptions;
 import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
+import org.yaml.snakeyaml.error.YAMLException;
+import org.yaml.snakeyaml.nodes.Tag;
 
 /**
  * Configure the import and export Yaml format
  */
 @Slf4j
 @Service
-public class YamlImExportServiceImpl extends AbstractImExportServiceImpl{
+public class YamlImExportServiceImpl extends AbstractImExportServiceImpl {
+
+    private static final Set<Tag> LEGACY_TAGS = Set.of(
+            new Tag(ExportMonitorDTO.class), new Tag(MonitorDTO.class), new 
Tag(ParamDTO.class));
 
     /**
      * Export file type
@@ -59,10 +69,27 @@ public class YamlImExportServiceImpl extends 
AbstractImExportServiceImpl{
      */
     @Override
     public List<ExportMonitorDTO> parseImport(InputStream is) {
-        // todo now disable this, will enable it in the future.
-        // upgrade to snakeyaml 2.2 and springboot3.x to fix the issue
-        Yaml yaml = new Yaml();
-        return yaml.load(is);
+        LoaderOptions options = new LoaderOptions();
+        options.setTagInspector(LEGACY_TAGS::contains);
+        Object data;
+        try {
+            data = new Yaml(new MonitorYamlConstructor(options)).load(is);
+        } catch (YAMLException e) {
+            throw new IllegalArgumentException("Invalid YAML monitor 
configuration", e);
+        }
+        if (!(data instanceof List<?> entries) || entries.isEmpty()) {
+            throw new IllegalArgumentException("YAML monitor configuration 
must be a non-empty list");
+        }
+        return entries.stream().map(entry -> {
+            if (!(entry instanceof Map<?, ?>)) {
+                throw new IllegalArgumentException("Each YAML monitor entry 
must be a mapping");
+            }
+            ExportMonitorDTO monitor = JsonUtil.convertValue(entry, 
ExportMonitorDTO.class);
+            if (monitor == null || monitor.getMonitor() == null) {
+                throw new IllegalArgumentException("Each YAML monitor entry 
must contain a valid monitor");
+            }
+            return monitor;
+        }).toList();
     }
 
     /**
@@ -73,7 +100,17 @@ public class YamlImExportServiceImpl extends 
AbstractImExportServiceImpl{
     @Override
     public void writeOs(List<ExportMonitorDTO> monitorList, OutputStream os) {
 
-        YamlExportUtils.exportWriteOs(monitorList, os);
+        // Export plain mappings so the file does not depend on Java class 
names.
+        YamlExportUtils.exportWriteOs(monitorList.stream()
+                .map(monitor -> JsonUtil.convertValue(monitor, 
Map.class)).toList(), os);
     }
 
+    private static final class MonitorYamlConstructor extends SafeConstructor {
+
+        private MonitorYamlConstructor(LoaderOptions options) {
+            super(options);
+            // Older exports contain DTO tags. Read only these tags as maps, 
never as Java objects.
+            LEGACY_TAGS.forEach(tag -> yamlConstructors.put(tag, new 
ConstructYamlMap()));
+        }
+    }
 }
diff --git 
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/YamlImExportServiceTest.java
 
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/YamlImExportServiceTest.java
index 341c99fff2..e9b15c9469 100644
--- 
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/YamlImExportServiceTest.java
+++ 
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/YamlImExportServiceTest.java
@@ -19,99 +19,196 @@ package org.apache.hertzbeat.manager.service;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
-import org.apache.hertzbeat.manager.service.impl.AbstractImExportServiceImpl;
+import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.common.entity.manager.Param;
+import org.apache.hertzbeat.common.util.export.YamlExportUtils;
+import org.apache.hertzbeat.manager.config.ManagerSseManager;
+import org.apache.hertzbeat.manager.pojo.dto.MonitorDto;
+import 
org.apache.hertzbeat.manager.service.impl.AbstractImExportServiceImpl.ExportMonitorDTO;
+import 
org.apache.hertzbeat.manager.service.impl.AbstractImExportServiceImpl.MonitorDTO;
+import 
org.apache.hertzbeat.manager.service.impl.AbstractImExportServiceImpl.ParamDTO;
 import org.apache.hertzbeat.manager.service.impl.YamlImExportServiceImpl;
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.ArgumentCaptor;
 import org.mockito.InjectMocks;
+import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.yaml.snakeyaml.Yaml;
 
 /**
- * Test case for {@link YamlImExportServiceImpl}
+ * Test case for {@link YamlImExportServiceImpl}.
  */
-
 @ExtendWith(MockitoExtension.class)
 class YamlImExportServiceTest {
 
     @InjectMocks
     private YamlImExportServiceImpl yamlImExportService;
 
-    @BeforeEach
-    void setUp() {
+    @Mock
+    private MonitorService monitorService;
 
-        yamlImExportService = new YamlImExportServiceImpl();
-    }
+    @Mock
+    private ManagerSseManager managerSseManager;
 
     @Test
     void testType() {
-
         assertEquals("YAML", yamlImExportService.type());
     }
 
     @Test
-    void testParseImport() {
+    void testParseImportCreatesTypedMonitorsAndParams() {
+        String yaml = """
+                - monitor:
+                    name: Monitor1
+                    app: linux
+                    intervals: 60
+                    status: 1
+                    labels: {env: prod}
+                  params:
+                    - field: port
+                      type: 0
+                      value: '9100'
+                - monitor:
+                    name: Monitor2
+                    app: linux
+                """;
+
+        List<ExportMonitorDTO> result = 
yamlImExportService.parseImport(input(yaml));
 
-        String yamlContent = "- id: 1\n  name: Monitor1\n- id: 2\n  name: 
Monitor2";
-        InputStream is = new 
ByteArrayInputStream(yamlContent.getBytes(StandardCharsets.UTF_8));
-
-        List<AbstractImExportServiceImpl.ExportMonitorDTO> result = 
yamlImExportService.parseImport(is);
-
-        assertNotNull(result);
         assertEquals(2, result.size());
-        assertEquals("[{id=1, name=Monitor1}, {id=2, name=Monitor2}]", 
result.toString());
+        assertEquals("Monitor1", result.get(0).getMonitor().getName());
+        assertEquals(60, result.get(0).getMonitor().getIntervals());
+        assertEquals(Map.of("env", "prod"), 
result.get(0).getMonitor().getLabels());
+        ParamDTO param = result.get(0).getParams().get(0);
+        assertEquals("port", param.getField());
+        assertEquals((byte) 0, param.getType());
+        assertEquals("9100", param.getValue());
+        assertEquals("Monitor2", result.get(1).getMonitor().getName());
     }
 
-    @Test
-    void testParseImportNull() {
-
-        InputStream is = new 
ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8));
+    @ParameterizedTest
+    @EmptySource
+    @ValueSource(strings = {"   ", "# comment", "null", "[]", "{}", "plain 
text", "- null", "- 1",
+            "- {}", "- monitor: null", "- monitor: invalid", "- monitor: 
{intervals: invalid}", "- monitor: ["})
+    void testInvalidImportDoesNotPersistMonitors(String yaml) {
+        assertThrows(IllegalArgumentException.class,
+                () -> yamlImExportService.importConfig("invalid.yaml", 
input(yaml)));
+        verifyNoInteractions(monitorService, managerSseManager);
+    }
 
-        List<AbstractImExportServiceImpl.ExportMonitorDTO> result = 
yamlImExportService.parseImport(is);
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "- !!java.util.HashMap {monitor: {name: test}}",
+            "- monitor: !!java.util.HashMap {name: test}",
+            "- monitor: {name: test}\n  params: [!!java.net.URL 
'https://example.com']"
+    })
+    void testRejectsUnapprovedGlobalTags(String yaml) {
+        IllegalArgumentException error = 
assertThrows(IllegalArgumentException.class,
+                () -> yamlImExportService.parseImport(input(yaml)));
+        assertTrue(error.getCause().getMessage().contains("Global tag is not 
allowed"));
+    }
 
-        assertNull(result);
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void testExportImportRoundTrip(boolean legacy) {
+        ExportMonitorDTO expected = exportedMonitor();
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        if (legacy) {
+            YamlExportUtils.exportWriteOs(List.of(expected), output);
+        } else {
+            yamlImExportService.writeOs(List.of(expected), output);
+        }
+        String yaml = output.toString(StandardCharsets.UTF_8);
+        if (legacy) {
+            assertTrue(yaml.contains("!!" + ExportMonitorDTO.class.getName()));
+        } else {
+            assertFalse(yaml.contains("!!"));
+            List<?> plainData = new Yaml().load(yaml);
+            assertInstanceOf(Map.class, plainData.get(0));
+        }
+
+        assertEquals(List.of(expected), 
yamlImExportService.parseImport(input(yaml)));
+        yamlImExportService.importConfig("monitors.yaml", input(yaml));
+
+        ArgumentCaptor<Monitor> monitorCaptor = 
ArgumentCaptor.forClass(Monitor.class);
+        ArgumentCaptor<List<Param>> paramsCaptor = 
ArgumentCaptor.forClass(List.class);
+        verify(monitorService).validate(any(MonitorDto.class), eq(false));
+        verify(monitorService).addMonitor(monitorCaptor.capture(), 
paramsCaptor.capture(), eq("collector-1"), isNull());
+        Monitor monitor = monitorCaptor.getValue();
+        assertEquals("Monitor1", monitor.getName());
+        assertEquals("linux", monitor.getApp());
+        assertEquals("localhost:9100", monitor.getInstance());
+        assertEquals(Map.of("env", "prod"), monitor.getLabels());
+        assertEquals(Map.of("owner", "ops"), monitor.getAnnotations());
+        assertEquals("cron", monitor.getScheduleType());
+        assertEquals("0 */5 * * * ?", monitor.getCronExpression());
+        assertEquals("HBA2-export-ciphertext", 
paramsCaptor.getValue().get(2).getParamValue());
+        verify(managerSseManager).broadcastImportTaskSuccess("monitors.yaml");
     }
 
     @Test
-    void testWriteOs() {
-
-        AbstractImExportServiceImpl.ParamDTO paramDTO = new 
AbstractImExportServiceImpl.ParamDTO();
-        paramDTO.setType((byte) 1);
-        paramDTO.setField("Test");
-        paramDTO.setValue("Test");
-        AbstractImExportServiceImpl.MonitorDTO monitorDTO = new 
AbstractImExportServiceImpl.MonitorDTO();
-        monitorDTO.setLabels(Map.of("env", "prod"));
-        monitorDTO.setIntervals(1);
-        monitorDTO.setStatus((byte) 1);
-        AbstractImExportServiceImpl.ExportMonitorDTO exportMonitorDto1 = new 
AbstractImExportServiceImpl.ExportMonitorDTO();
-        exportMonitorDto1.setParams(List.of(paramDTO));
-        exportMonitorDto1.setMonitor(monitorDTO);
-        AbstractImExportServiceImpl.ExportMonitorDTO exportMonitorDto2 = new 
AbstractImExportServiceImpl.ExportMonitorDTO();
-        exportMonitorDto2.setParams(List.of(paramDTO));
-        exportMonitorDto2.setMonitor(monitorDTO);
-
-        List<AbstractImExportServiceImpl.ExportMonitorDTO> monitorList = 
Arrays.asList(
-                exportMonitorDto1,
-                exportMonitorDto2
-        );
-        OutputStream os = new ByteArrayOutputStream();
-
-        yamlImExportService.writeOs(monitorList, os);
-
-        String output = os.toString();
-        assertFalse(output.contains("metrics:\n  - Test1"));
-        assertTrue(output.contains("  params:\n  - &id002\n    field: Test"));
+    void testExportConfigProducesImportableYaml() {
+        MonitorDto source = new MonitorDto();
+        
source.setMonitor(Monitor.builder().id(1L).name("Monitor1").app("linux").build());
+        source.setParams(List.of());
+        source.setCollector("collector-1");
+        when(monitorService.getMonitorDtoForExport(1L)).thenReturn(source);
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+
+        yamlImExportService.exportConfig(output, List.of(1L));
+        yamlImExportService.importConfig("monitors.yaml", new 
ByteArrayInputStream(output.toByteArray()));
+
+        verify(monitorService).addMonitor(any(Monitor.class), eq(List.of()), 
eq("collector-1"), isNull());
+        verify(managerSseManager).broadcastImportTaskSuccess("monitors.yaml");
+    }
+
+    private ExportMonitorDTO exportedMonitor() {
+        MonitorDTO monitor = new MonitorDTO();
+        monitor.setName("Monitor1");
+        monitor.setApp("linux");
+        monitor.setHost("localhost");
+        monitor.setIntervals(60);
+        monitor.setStatus((byte) 1);
+        monitor.setLabels(Map.of("env", "prod"));
+        monitor.setAnnotations(Map.of("owner", "ops"));
+        monitor.setScheduleType("cron");
+        monitor.setCronExpression("0 */5 * * * ?");
+        monitor.setCollector("collector-1");
+        ExportMonitorDTO dto = new ExportMonitorDTO();
+        dto.setMonitor(monitor);
+        dto.setParams(List.of(param("host", (byte) 1, "localhost"), 
param("port", (byte) 0, "9100"),
+                param("password", (byte) 2, "HBA2-export-ciphertext")));
+        return dto;
     }
 
+    private ParamDTO param(String field, byte type, String value) {
+        ParamDTO param = new ParamDTO();
+        param.setField(field);
+        param.setType(type);
+        param.setValue(value);
+        return param;
+    }
+
+    private ByteArrayInputStream input(String yaml) {
+        return new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8));
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to