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 a08a365952 fix(collector): keep partial telnet metrics, surface raw 
reply (#4311)
a08a365952 is described below

commit a08a36595273fa2f60f19d5f88757c02cd28b27f
Author: NekoPunch <[email protected]>
AuthorDate: Sun Aug 16 20:33:06 2026 -0700

    fix(collector): keep partial telnet metrics, surface raw reply (#4311)
    
    Co-authored-by: aias00 <[email protected]>
---
 .../collect/telnet/TelnetCollectImpl.java          |  48 +++++----
 .../collect/telnet/TelnetCollectImplTest.java      | 110 +++++++++++++++++++++
 2 files changed, 140 insertions(+), 18 deletions(-)

diff --git 
a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/telnet/TelnetCollectImpl.java
 
b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/telnet/TelnetCollectImpl.java
index 20602caced..e10860cef0 100644
--- 
a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/telnet/TelnetCollectImpl.java
+++ 
b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/telnet/TelnetCollectImpl.java
@@ -67,14 +67,20 @@ public class TelnetCollectImpl extends AbstractCollect {
                 long responseTime = System.currentTimeMillis() - startTime;
                 List<String> aliasFields = metrics.getAliasFields();
                 String app = builder.getApp();
-                Map<String, String> resultMap = 
execCmdAndParseResult(telnetClient, telnet.getCmd(), app);
-                resultMap.put(CollectorConstants.RESPONSE_TIME, 
Long.toString(responseTime));
-                if (resultMap.size() < aliasFields.size()) {
-                    log.error("telnet response data not enough: {}", 
resultMap);
+                CmdResult cmdResult = execCmdAndParseResult(telnetClient, 
telnet.getCmd(), app);
+                Map<String, String> resultMap = cmdResult.values();
+                boolean expectsCmdMetrics = 
StringUtils.isNotBlank(telnet.getCmd())
+                        && aliasFields.stream().anyMatch(field -> 
!CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(field));
+                boolean hasExpectedMetric = 
aliasFields.stream().anyMatch(resultMap::containsKey);
+                if (expectsCmdMetrics && !hasExpectedMetric) {
+                    // e.g. zookeeper refusing a 4lw command not in its 
4lw.commands.whitelist
+                    String reply = sanitizeReply(cmdResult.rawResponse());
+                    log.warn("telnet cmd [{}] returned no expected metrics: 
{}", telnet.getCmd(), reply);
                     builder.setCode(CollectRep.Code.FAIL);
-                    builder.setMsg("The cmd execution results do not match the 
expected number of metrics.");
+                    builder.setMsg("Cmd [" + telnet.getCmd() + "] returned no 
expected metrics. Response: " + reply);
                     return;
                 }
+                resultMap.put(CollectorConstants.RESPONSE_TIME, 
Long.toString(responseTime));
                 CollectRep.ValueRow.Builder valueRowBuilder = 
CollectRep.ValueRow.newBuilder();
                 for (String field : aliasFields) {
                     String fieldValue = resultMap.get(field);
@@ -118,30 +124,36 @@ public class TelnetCollectImpl extends AbstractCollect {
         return DispatchConstants.PROTOCOL_TELNET;
     }
 
-    private static Map<String, String> execCmdAndParseResult(TelnetClient 
telnetClient, String cmd, String app) throws IOException {
+    record CmdResult(Map<String, String> values, String rawResponse) {
+    }
+
+    private static String sanitizeReply(String raw) {
+        return StringUtils.abbreviate(raw.trim().replaceAll("[\\p{Cntrl}]+", " 
"), 300);
+    }
+
+    private static CmdResult execCmdAndParseResult(TelnetClient telnetClient, 
String cmd, String app) throws IOException {
         if (cmd == null || StringUtils.isEmpty(cmd.trim())) {
-            return new HashMap<>(16);
+            return new CmdResult(new HashMap<>(16), "");
         }
         OutputStream outputStream = telnetClient.getOutputStream();
         outputStream.write(cmd.getBytes(StandardCharsets.UTF_8));
         outputStream.flush();
         String result = new 
String(telnetClient.getInputStream().readAllBytes());
         String[] lines = result.split("\n");
-        if (CollectorConstants.ZOOKEEPER_APP.equals(app) && 
CollectorConstants.ZOOKEEPER_ENVI_HEAD.equals(lines[0])) {
+        if (lines.length > 0 && CollectorConstants.ZOOKEEPER_APP.equals(app)
+                && CollectorConstants.ZOOKEEPER_ENVI_HEAD.equals(lines[0])) {
             lines = Arrays.stream(lines)
                     .skip(1)
                     .toArray(String[]::new);
         }
-        boolean contains = lines[0].contains("=");
-        return Arrays.stream(lines)
-                .map(item -> {
-                    if (contains) {
-                        return item.split("=");
-                    } else {
-                        return item.split("\t");
-                    }
-                })
+        if (lines.length == 0) {
+            return new CmdResult(new HashMap<>(16), result);
+        }
+        String separator = lines[0].contains("=") ? "=" : "\t";
+        Map<String, String> values = Arrays.stream(lines)
+                .map(item -> item.split(separator, 2))
                 .filter(item -> item.length == 2)
-                .collect(Collectors.toMap(x -> x[0], x -> x[1]));
+                .collect(Collectors.toMap(x -> x[0], x -> x[1], (first, 
second) -> first, HashMap::new));
+        return new CmdResult(values, result);
     }
 }
diff --git 
a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/telnet/TelnetCollectImplTest.java
 
b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/telnet/TelnetCollectImplTest.java
index 0c1b724a8e..d824abe42a 100644
--- 
a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/telnet/TelnetCollectImplTest.java
+++ 
b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/telnet/TelnetCollectImplTest.java
@@ -21,6 +21,7 @@ import static 
org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
@@ -30,6 +31,7 @@ import java.util.ArrayList;
 import java.util.List;
 import org.apache.commons.net.telnet.TelnetClient;
 import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
+import org.apache.hertzbeat.common.constants.CommonConstants;
 import org.apache.hertzbeat.common.entity.job.Metrics;
 import org.apache.hertzbeat.common.entity.job.protocol.TelnetProtocol;
 import org.apache.hertzbeat.common.entity.message.CollectRep;
@@ -155,6 +157,114 @@ class TelnetCollectImplTest {
         mocked.close();
     }
 
+    @Test
+    void testCollectPadsMissingMetrics() {
+        CollectRep.MetricsData.Builder builder = 
CollectRep.MetricsData.newBuilder();
+        Metrics metrics = telnetMetrics("mntr", List.of("responseTime", "a", 
"b", "c"));
+        try (MockedConstruction<TelnetClient> mocked = mockTelnetReply("a=1")) 
{
+            telnetCollect.collect(builder, metrics);
+        }
+        assertEquals(1, builder.getValuesCount());
+        assertEquals("1", builder.getValues(0).getColumns(1));
+        assertEquals(CommonConstants.NULL_VALUE, 
builder.getValues(0).getColumns(2));
+        assertEquals(CommonConstants.NULL_VALUE, 
builder.getValues(0).getColumns(3));
+    }
+
+    @Test
+    void testCollectFailsWithRawReplyWhenNothingParsed() {
+        CollectRep.MetricsData.Builder builder = 
CollectRep.MetricsData.newBuilder();
+        Metrics metrics = telnetMetrics("conf", List.of("responseTime", "a"));
+        try (MockedConstruction<TelnetClient> mocked =
+                mockTelnetReply("conf is not executed because it is not in the 
whitelist.")) {
+            telnetCollect.collect(builder, metrics);
+        }
+        assertEquals(CollectRep.Code.FAIL, builder.getCode());
+        assertTrue(builder.getMsg().contains("not in the whitelist"));
+    }
+
+    @Test
+    void testCollectKeepsFirstOnDuplicateKeys() {
+        CollectRep.MetricsData.Builder builder = 
CollectRep.MetricsData.newBuilder();
+        Metrics metrics = telnetMetrics("mntr", List.of("a", "b"));
+        try (MockedConstruction<TelnetClient> mocked = 
mockTelnetReply("a=1\na=2\nb=3")) {
+            telnetCollect.collect(builder, metrics);
+        }
+        assertEquals(1, builder.getValuesCount());
+        assertEquals("1", builder.getValues(0).getColumns(0));
+        assertEquals("3", builder.getValues(0).getColumns(1));
+    }
+
+    @Test
+    void testCollectFailsWhenReplyHasOnlyUnrelatedPairs() {
+        CollectRep.MetricsData.Builder builder = 
CollectRep.MetricsData.newBuilder();
+        Metrics metrics = telnetMetrics("mntr", List.of("responseTime", "a"));
+        try (MockedConstruction<TelnetClient> mocked = 
mockTelnetReply("error=conf disabled")) {
+            telnetCollect.collect(builder, metrics);
+        }
+        assertEquals(CollectRep.Code.FAIL, builder.getCode());
+    }
+
+    @Test
+    void testCollectSurvivesHeaderOnlyEnviReply() {
+        CollectRep.MetricsData.Builder builder = 
CollectRep.MetricsData.newBuilder().setApp("zookeeper");
+        Metrics metrics = telnetMetrics("envi", List.of("responseTime", "a"));
+        try (MockedConstruction<TelnetClient> mocked = 
mockTelnetReply("Environment:")) {
+            telnetCollect.collect(builder, metrics);
+        }
+        assertEquals(CollectRep.Code.FAIL, builder.getCode());
+    }
+
+    @Test
+    void testCollectFailsCleanlyOnNewlineOnlyReply() {
+        CollectRep.MetricsData.Builder builder = 
CollectRep.MetricsData.newBuilder().setApp("zookeeper");
+        Metrics metrics = telnetMetrics("conf", List.of("responseTime", "a"));
+        try (MockedConstruction<TelnetClient> mocked = mockTelnetReply("\n")) {
+            telnetCollect.collect(builder, metrics);
+        }
+        assertEquals(CollectRep.Code.FAIL, builder.getCode());
+        assertTrue(builder.getMsg().contains("returned no expected metrics"));
+    }
+
+    @Test
+    void testCollectKeepsValueContainingSeparator() {
+        CollectRep.MetricsData.Builder builder = 
CollectRep.MetricsData.newBuilder();
+        Metrics metrics = telnetMetrics("conf", List.of("dataDir", 
"secureClientPort"));
+        try (MockedConstruction<TelnetClient> mocked = 
mockTelnetReply("dataDir=/data/zk=a\nsecureClientPort=")) {
+            telnetCollect.collect(builder, metrics);
+        }
+        assertEquals(1, builder.getValuesCount());
+        assertEquals("/data/zk=a", builder.getValues(0).getColumns(0));
+        assertEquals("", builder.getValues(0).getColumns(1));
+    }
+
+    @Test
+    void testCollectBlankCmdKeepsResponseTimeRow() {
+        CollectRep.MetricsData.Builder builder = 
CollectRep.MetricsData.newBuilder();
+        Metrics metrics = telnetMetrics("", List.of("responseTime"));
+        try (MockedConstruction<TelnetClient> mocked =
+                Mockito.mockConstruction(TelnetClient.class, (telnetClient, 
context) ->
+                        
Mockito.when(telnetClient.isConnected()).thenReturn(true))) {
+            telnetCollect.collect(builder, metrics);
+        }
+        assertEquals(1, builder.getValuesCount());
+    }
+
+    private static Metrics telnetMetrics(String cmd, List<String> aliasFields) 
{
+        Metrics metrics = new Metrics();
+        
metrics.setTelnet(TelnetProtocol.builder().timeout("10").port("2181").cmd(cmd).build());
+        metrics.setAliasFields(aliasFields);
+        return metrics;
+    }
+
+    private static MockedConstruction<TelnetClient> mockTelnetReply(String 
reply) {
+        InputStream inputStream = new 
ByteArrayInputStream(reply.getBytes(StandardCharsets.UTF_8));
+        return Mockito.mockConstruction(TelnetClient.class, (telnetClient, 
context) -> {
+            Mockito.when(telnetClient.isConnected()).thenReturn(true);
+            
Mockito.when(telnetClient.getOutputStream()).thenReturn(Mockito.mock(OutputStream.class));
+            
Mockito.when(telnetClient.getInputStream()).thenReturn(inputStream);
+        });
+    }
+
     @Test
     void preCheck() throws IllegalArgumentException {
         // metrics is null


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

Reply via email to