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]