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

Duansg 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 db73a43f9e maintenance: define VictoriaMetrics label collisions (#4286)
db73a43f9e is described below

commit db73a43f9e34e3252b908d461157261f08f6c965
Author: Logic <[email protected]>
AuthorDate: Wed Aug 26 00:46:27 2026 +0800

    maintenance: define VictoriaMetrics label collisions (#4286)
    
    Co-authored-by: Duansg <[email protected]>
---
 .../tsdb/greptime/GreptimeDbDataStorage.java       |  35 +++++++
 .../tsdb/vm/VictoriaMetricsClusterDataStorage.java |  30 +++++-
 .../tsdb/vm/VictoriaMetricsDataStorage.java        |  68 ++++++++++++-
 .../tsdb/greptime/GreptimeDbDataStorageTest.java   |  54 ++++++-----
 .../tsdb/vm/VictoriaMetricsDataStorageTest.java    | 107 ++++++++++++++++++++-
 home/docs/start/victoria-metrics-init.md           |  19 ++++
 6 files changed, 280 insertions(+), 33 deletions(-)

diff --git 
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java
 
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java
index 5ccbe4b84a..ca3b8cf142 100644
--- 
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java
+++ 
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java
@@ -45,9 +45,12 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.BiConsumer;
 import java.util.function.Function;
 import java.util.stream.Collectors;
@@ -113,6 +116,7 @@ public class GreptimeDbDataStorage extends 
AbstractHistoryDataStorage {
     private final RestTemplate restTemplate;
 
     private final GreptimeSqlQueryExecutor greptimeSqlQueryExecutor;
+    private final AtomicLong ignoredLabelCollisionCount = new AtomicLong();
 
     public GreptimeDbDataStorage(GreptimeProperties greptimeProperties,
                                  
@Qualifier(WarehouseConstants.GREPTIME_QUERY_REST_TEMPLATE)
@@ -164,6 +168,16 @@ public class GreptimeDbDataStorage extends 
AbstractHistoryDataStorage {
         List<CollectRep.Field> fields = metricsData.getFields();
         Map<String, String> customLabels = metricsData.getLabels();
         List<String> fieldNames = 
fields.stream().map(CollectRep.Field::getName).collect(Collectors.toList());
+        Set<String> labelCollisions = findLabelCollisions(customLabels, 
fieldNames);
+        if (!labelCollisions.isEmpty()) {
+            long previousCount = 
ignoredLabelCollisionCount.getAndAdd(labelCollisions.size());
+            long ignoredCount = previousCount + labelCollisions.size();
+            if (shouldLogLabelCollisions(previousCount, ignoredCount)) {
+                log.warn("[warehouse greptime] ignore custom labels {} from 
metrics data {} because "
+                                + "the keys are storage-managed; cumulative 
ignored labels: {}.",
+                        labelCollisions, metricsData.getId(), ignoredCount);
+            }
+        }
         fields.forEach(field -> {
             if (field.getLabel()) {
                 tableSchemaBuilder.addTag(field.getName(), DataType.String);
@@ -237,6 +251,27 @@ public class GreptimeDbDataStorage extends 
AbstractHistoryDataStorage {
         }
     }
 
+    private Set<String> findLabelCollisions(Map<String, String> customLabels, 
List<String> fieldNames) {
+        if (customLabels == null || customLabels.isEmpty()) {
+            return Set.of();
+        }
+        Set<String> collisions = new TreeSet<>();
+        for (String key : customLabels.keySet()) {
+            if (LABEL_KEY_INSTANCE.equals(key) || LABEL_KEY_TS.equals(key) || 
fieldNames.contains(key)) {
+                collisions.add(key);
+            }
+        }
+        return collisions;
+    }
+
+    private boolean shouldLogLabelCollisions(long previousCount, long 
currentCount) {
+        return previousCount == 0 || previousCount / 100 < currentCount / 100;
+    }
+
+    long getIgnoredLabelCollisionCount() {
+        return ignoredLabelCollisionCount.get();
+    }
+
     @Override
     public Map<String, List<Value>> getHistoryMetricData(String instance, 
String app, String metrics, String metric,
                                                          String history) {
diff --git 
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsClusterDataStorage.java
 
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsClusterDataStorage.java
index ae93a1e7e2..3777bb1830 100644
--- 
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsClusterDataStorage.java
+++ 
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsClusterDataStorage.java
@@ -33,9 +33,11 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -69,7 +71,6 @@ import org.springframework.http.HttpMethod;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Component;
-import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
 import org.springframework.web.client.RestTemplate;
 import org.springframework.web.util.UriComponentsBuilder;
@@ -107,6 +108,7 @@ public class VictoriaMetricsClusterDataStorage extends 
AbstractHistoryDataStorag
     private final VictoriaMetricsSelectProperties vmSelectProps;
     private final RestTemplate restTemplate;
     private final 
BlockingQueue<VictoriaMetricsDataStorage.VictoriaMetricsContent> 
metricsBufferQueue;
+    private final AtomicLong ignoredLabelCollisionCount = new AtomicLong();
 
     private HashedWheelTimer metricsFlushTimer = null;
     private MetricsFlushTask metricsFlushtask = null;
@@ -186,6 +188,13 @@ public class VictoriaMetricsClusterDataStorage extends 
AbstractHistoryDataStorag
                 metricsData.getId(), metricsData.getApp(), 
metricsData.getMetrics());
             return;
         }
+        var managedLabelCollisions =
+                
VictoriaMetricsDataStorage.findManagedLabelCollisions(metricsData.getLabels());
+        if (!managedLabelCollisions.isEmpty()) {
+            recordIgnoredLabelCollisions(metricsData.getId(), 
managedLabelCollisions);
+        }
+        Map<String, String> customizedLabels = 
VictoriaMetricsDataStorage.withoutManagedLabels(
+                metricsData.getLabels(), managedLabelCollisions);
         Map<String, String> defaultLabels = Maps.newHashMapWithExpectedSize(8);
         defaultLabels.put(MONITOR_METRICS_KEY, metricsData.getMetrics());
         boolean isPrometheusAuto;
@@ -243,10 +252,7 @@ public class VictoriaMetricsClusterDataStorage extends 
AbstractHistoryDataStorag
                                 }
                                 labels.put(LABEL_KEY_MONITOR_ID, 
String.valueOf(metricsData.getId()));
                                 // add customized labels as identifier
-                                var customizedLabels = metricsData.getLabels();
-                                if (!ObjectUtils.isEmpty(customizedLabels)) {
-                                    labels.putAll(customizedLabels);
-                                }
+                                
VictoriaMetricsDataStorage.addCustomizedLabels(labels, customizedLabels);
                                 
VictoriaMetricsDataStorage.VictoriaMetricsContent content = 
VictoriaMetricsDataStorage.VictoriaMetricsContent.builder()
                                     .metric(new HashMap<>(labels))
                                     .values(new Double[]{entry.getValue()})
@@ -276,6 +282,20 @@ public class VictoriaMetricsClusterDataStorage extends 
AbstractHistoryDataStorag
         }
     }
 
+    private void recordIgnoredLabelCollisions(long monitorId, Set<String> 
collisions) {
+        long previousCount = 
ignoredLabelCollisionCount.getAndAdd(collisions.size());
+        long ignoredCount = previousCount + collisions.size();
+        if (previousCount == 0 || previousCount / 100 < ignoredCount / 100) {
+            log.warn("[warehouse victoria-metrics-cluster] ignore custom 
labels {} from metrics data {} because "
+                            + "the keys are HertzBeat-managed; cumulative 
ignored labels: {}.",
+                    collisions, monitorId, ignoredCount);
+        }
+    }
+
+    long getIgnoredLabelCollisionCount() {
+        return ignoredLabelCollisionCount.get();
+    }
+
     @Override
     public void destroy() {
         if (metricsFlushTimer != null && !metricsFlushTimer.isStop()) {
diff --git 
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsDataStorage.java
 
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsDataStorage.java
index 5714e42186..5a80c9824c 100644
--- 
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsDataStorage.java
+++ 
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsDataStorage.java
@@ -32,10 +32,13 @@ import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.zip.GZIPOutputStream;
 
 import com.google.common.collect.Maps;
@@ -98,12 +101,19 @@ public class VictoriaMetricsDataStorage extends 
AbstractHistoryDataStorage {
     private static final String SPILT = "_";
     private static final String MONITOR_METRICS_KEY = "__metrics__";
     private static final String MONITOR_METRIC_KEY = "__metric__";
+    private static final Set<String> MANAGED_LABEL_KEYS = Set.of(
+        LABEL_KEY_NAME,
+        LABEL_KEY_MONITOR_ID,
+        MONITOR_METRICS_KEY,
+        MONITOR_METRIC_KEY,
+        LABEL_KEY_INSTANCE);
     private static final long MAX_WAIT_MS = 500L;
     private static final int MAX_RETRIES = 3;
 
     private final VictoriaMetricsProperties victoriaMetricsProp;
     private final RestTemplate restTemplate;
     private final 
BlockingQueue<VictoriaMetricsDataStorage.VictoriaMetricsContent> 
metricsBufferQueue;
+    private final AtomicLong ignoredLabelCollisionCount = new AtomicLong();
 
     private HashedWheelTimer metricsFlushTimer = null;
     private final VictoriaMetricsProperties.InsertConfig insertConfig;
@@ -170,6 +180,12 @@ public class VictoriaMetricsDataStorage extends 
AbstractHistoryDataStorage {
                 metricsData.getId(), metricsData.getApp(), 
metricsData.getMetrics());
             return;
         }
+        Set<String> managedLabelCollisions = 
findManagedLabelCollisions(metricsData.getLabels());
+        if (!managedLabelCollisions.isEmpty()) {
+            recordIgnoredLabelCollisions(metricsData.getId(), 
managedLabelCollisions);
+        }
+        Map<String, String> customizedLabels = withoutManagedLabels(
+                metricsData.getLabels(), managedLabelCollisions);
         Map<String, String> defaultLabels = Maps.newHashMapWithExpectedSize(8);
         defaultLabels.put(MONITOR_METRICS_KEY, metricsData.getMetrics());
         boolean isPrometheusAuto = false;
@@ -226,10 +242,7 @@ public class VictoriaMetricsDataStorage extends 
AbstractHistoryDataStorage {
                             }
                             labels.put(LABEL_KEY_MONITOR_ID, 
String.valueOf(metricsData.getId()));
                             // add customized labels as identifier
-                            var customizedLabels = metricsData.getLabels();
-                            if (!ObjectUtils.isEmpty(customizedLabels)) {
-                                labels.putAll(customizedLabels);
-                            }
+                            addCustomizedLabels(labels, customizedLabels);
                             VictoriaMetricsContent content = 
VictoriaMetricsContent.builder()
                                 .metric(new HashMap<>(labels))
                                 .values(new Double[]{entry.getValue()})
@@ -255,6 +268,53 @@ public class VictoriaMetricsDataStorage extends 
AbstractHistoryDataStorage {
         sendVictoriaMetrics(contentList);
     }
 
+    static void addCustomizedLabels(Map<String, String> labels, Map<String, 
String> customizedLabels) {
+        if (ObjectUtils.isEmpty(customizedLabels)) {
+            return;
+        }
+        labels.putAll(customizedLabels);
+    }
+
+    private void recordIgnoredLabelCollisions(long monitorId, Set<String> 
collisions) {
+        long previousCount = 
ignoredLabelCollisionCount.getAndAdd(collisions.size());
+        long ignoredCount = previousCount + collisions.size();
+        if (previousCount == 0 || previousCount / 100 < ignoredCount / 100) {
+            log.warn("[warehouse victoria-metrics] ignore custom labels {} 
from metrics data {} because "
+                            + "the keys are HertzBeat-managed; cumulative 
ignored labels: {}.",
+                    collisions, monitorId, ignoredCount);
+        }
+    }
+
+    long getIgnoredLabelCollisionCount() {
+        return ignoredLabelCollisionCount.get();
+    }
+
+    static Set<String> findManagedLabelCollisions(Map<String, String> 
customizedLabels) {
+        if (ObjectUtils.isEmpty(customizedLabels)) {
+            return Set.of();
+        }
+        Set<String> collisions = new TreeSet<>();
+        for (String key : customizedLabels.keySet()) {
+            if (key != null && MANAGED_LABEL_KEYS.contains(key)) {
+                collisions.add(key);
+            }
+        }
+        return collisions;
+    }
+
+    static Map<String, String> withoutManagedLabels(
+            Map<String, String> customizedLabels, Set<String> 
managedLabelCollisions) {
+        if (ObjectUtils.isEmpty(customizedLabels)) {
+            return Map.of();
+        }
+        if (managedLabelCollisions.isEmpty()) {
+            return customizedLabels;
+        }
+        Map<String, String> safeLabels = new HashMap<>(customizedLabels);
+        managedLabelCollisions.forEach(safeLabels::remove);
+        return safeLabels;
+    }
+
     @Override
     public void destroy() {
         if (metricsFlushTimer != null && !metricsFlushTimer.isStop()) {
diff --git 
a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorageTest.java
 
b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorageTest.java
index 91e4ccac62..2e1a3ba980 100644
--- 
a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorageTest.java
+++ 
b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorageTest.java
@@ -38,7 +38,6 @@ import io.greptime.models.Err;
 import io.greptime.models.Result;
 import io.greptime.models.Table;
 import io.greptime.models.WriteOk;
-import io.greptime.v1.Common;
 import io.greptime.v1.RowData;
 import java.lang.reflect.Field;
 import java.util.ArrayList;
@@ -48,7 +47,6 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
-import java.util.stream.Collectors;
 import org.apache.hertzbeat.common.constants.CommonConstants;
 import org.apache.hertzbeat.common.entity.arrow.ArrowCell;
 import org.apache.hertzbeat.common.entity.arrow.RowWrapper;
@@ -133,7 +131,6 @@ class GreptimeDbDataStorageTest {
             when(mockResult.isOk()).thenReturn(true);
             CompletableFuture<Result<WriteOk, Err>> mockFuture = 
CompletableFuture.completedFuture(mockResult);
             when(greptimeDb.write(any(Table.class))).thenReturn(mockFuture);
-
             greptimeDbDataStorage = new 
GreptimeDbDataStorage(greptimeProperties, restTemplate, 
greptimeSqlQueryExecutor);
 
             // Test with valid metrics data
@@ -157,15 +154,14 @@ class GreptimeDbDataStorageTest {
     }
 
     @Test
-    void testSaveDataWithCustomLabels() throws Exception {
+    void testSaveDataSkipsCustomLabelCollisionsWithoutDroppingMetrics() throws 
Exception {
         try (MockedStatic<GreptimeDB> mockedStatic = 
mockStatic(GreptimeDB.class)) {
             mockedStatic.when(() -> 
GreptimeDB.create(any())).thenReturn(greptimeDb);
-
+            @SuppressWarnings("unchecked")
             Result<WriteOk, Err> mockResult = mock(Result.class);
             when(mockResult.isOk()).thenReturn(true);
-            CompletableFuture<Result<WriteOk, Err>> mockFuture = 
CompletableFuture.completedFuture(mockResult);
-            when(greptimeDb.write(any(Table.class))).thenReturn(mockFuture);
-
+            when(greptimeDb.write(any(Table.class)))
+                    .thenReturn(CompletableFuture.completedFuture(mockResult));
             greptimeDbDataStorage = new 
GreptimeDbDataStorage(greptimeProperties, restTemplate, 
greptimeSqlQueryExecutor);
 
             CollectRep.MetricsData metricsData = createMockMetricsData(true);
@@ -181,27 +177,39 @@ class GreptimeDbDataStorageTest {
             greptimeDbDataStorage.saveData(metricsData);
 
             verify(greptimeDb).write(tableCaptor.capture());
-            Table capturedTable = tableCaptor.getValue();
-
-            List<RowData.ColumnSchema> columnSchemas = 
getColumnSchemas(capturedTable);
-            List<String> columnNames = columnSchemas.stream()
+            List<String> columnNames = 
getColumnSchemas(tableCaptor.getValue()).stream()
                     .map(RowData.ColumnSchema::getColumnName)
-                    .collect(Collectors.toList());
-            assertEquals(5, columnNames.size());
+                    .toList();
+            // The fixture already contains an `instance` metric field in 
addition to the
+            // storage identity tag; the conflicting custom label must not add 
a third column.
             assertEquals(2, Collections.frequency(columnNames, "instance"));
             assertEquals(1, Collections.frequency(columnNames, "ts"));
             assertEquals(1, Collections.frequency(columnNames, "usage"));
+            assertEquals(1, Collections.frequency(columnNames, "env"));
+            assertEquals(3, 
greptimeDbDataStorage.getIgnoredLabelCollisionCount());
+        }
+    }
+
+    @Test
+    void testSaveDataAcceptsNonConflictingCustomLabels() throws Exception {
+        try (MockedStatic<GreptimeDB> mockedStatic = 
mockStatic(GreptimeDB.class)) {
+            mockedStatic.when(() -> 
GreptimeDB.create(any())).thenReturn(greptimeDb);
+            @SuppressWarnings("unchecked")
+            Result<WriteOk, Err> mockResult = mock(Result.class);
+            when(mockResult.isOk()).thenReturn(true);
+            when(greptimeDb.write(any(Table.class)))
+                    .thenReturn(CompletableFuture.completedFuture(mockResult));
+            greptimeDbDataStorage = new GreptimeDbDataStorage(
+                    greptimeProperties, restTemplate, 
greptimeSqlQueryExecutor);
+            CollectRep.MetricsData metricsData = createMockMetricsData(true);
+            when(metricsData.getLabels()).thenReturn(Map.of("env", "prod"));
 
-            List<RowData.ColumnSchema> envColumnSchemas = 
columnSchemas.stream()
-                    .filter(columnSchema -> 
"env".equals(columnSchema.getColumnName()))
-                    .collect(Collectors.toList());
-            assertEquals(1, envColumnSchemas.size());
-            assertEquals(Common.SemanticType.TAG, 
envColumnSchemas.get(0).getSemanticType());
+            ArgumentCaptor<Table> tableCaptor = 
ArgumentCaptor.forClass(Table.class);
+            greptimeDbDataStorage.saveData(metricsData);
 
-            List<RowData.Row> rows = getRows(capturedTable);
-            assertEquals(1, rows.size());
-            RowData.Row row = rows.get(0);
-            assertEquals("prod", row.getValuesList().get(4).getStringValue());
+            verify(greptimeDb).write(tableCaptor.capture());
+            List<RowData.ColumnSchema> schemas = 
getColumnSchemas(tableCaptor.getValue());
+            assertTrue(schemas.stream().anyMatch(schema -> 
"env".equals(schema.getColumnName())));
         }
     }
 
diff --git 
a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsDataStorageTest.java
 
b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsDataStorageTest.java
index f11fdb905d..dd0ac7d9f3 100644
--- 
a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsDataStorageTest.java
+++ 
b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsDataStorageTest.java
@@ -32,6 +32,7 @@ import 
org.apache.hertzbeat.common.constants.MetricDataConstants;
 import org.apache.hertzbeat.common.entity.arrow.ArrowCell;
 import org.apache.hertzbeat.common.entity.arrow.RowWrapper;
 import org.apache.hertzbeat.common.entity.message.CollectRep;
+import org.apache.hertzbeat.common.util.JsonUtil;
 import org.awaitility.Awaitility;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
@@ -48,16 +49,20 @@ import org.springframework.http.HttpEntity;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
+import org.springframework.boot.test.system.CapturedOutput;
+import org.springframework.boot.test.system.OutputCaptureExtension;
 import org.springframework.web.client.RestTemplate;
 
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * Test case for {@link VictoriaMetricsDataStorage}
  */
-@ExtendWith(MockitoExtension.class)
+@ExtendWith({MockitoExtension.class, OutputCaptureExtension.class})
 @MockitoSettings(strictness = Strictness.LENIENT)
 class VictoriaMetricsDataStorageTest {
 
@@ -73,6 +78,7 @@ class VictoriaMetricsDataStorageTest {
     private VictoriaMetricsDataStorage victoriaMetricsDataStorage;
 
     private final AtomicInteger postForEntityCount = new AtomicInteger(0);
+    private final AtomicReference<String> lastPayload = new 
AtomicReference<>();
 
     @BeforeEach
     void setUp() {
@@ -97,6 +103,10 @@ class VictoriaMetricsDataStorageTest {
                 eq(String.class)
         )).thenAnswer(invocation -> {
             postForEntityCount.incrementAndGet();
+            HttpEntity<?> httpEntity = invocation.getArgument(1);
+            if (httpEntity.getBody() instanceof String payload) {
+                lastPayload.set(payload);
+            }
             return responseEntity;
         });
     }
@@ -184,6 +194,99 @@ class VictoriaMetricsDataStorageTest {
                                 .isGreaterThanOrEqualTo(threadCount * 
writeSize / bufferSize));
     }
 
+    @Test
+    void customLabelsKeepJobButCannotOverrideStorageInstance() {
+        when(victoriaMetricsProperties.insert()).thenReturn(new 
VictoriaMetricsProperties.InsertConfig(
+                1, Integer.MAX_VALUE, new 
VictoriaMetricsProperties.Compression(false)));
+        CollectRep.MetricsData metricsData = generateMockedMetricsData();
+        when(metricsData.getLabels()).thenReturn(Map.of(
+                "job", "custom-job",
+                "instance", "custom-instance",
+                "region", "west"));
+        victoriaMetricsDataStorage = new 
VictoriaMetricsDataStorage(victoriaMetricsProperties, restTemplate);
+
+        victoriaMetricsDataStorage.saveData(metricsData);
+
+        Awaitility.await()
+                .atMost(5, TimeUnit.SECONDS)
+                .untilAsserted(() -> 
assertThat(postForEntityCount.get()).isEqualTo(1));
+        VictoriaMetricsDataStorage.VictoriaMetricsContent content =
+                JsonUtil.fromJson(lastPayload.get().trim(), 
VictoriaMetricsDataStorage.VictoriaMetricsContent.class);
+        assertThat(content.getMetric())
+                .containsEntry("job", "custom-job")
+                .containsEntry("instance", "storage-instance")
+                .containsEntry("region", "west");
+        
assertThat(victoriaMetricsDataStorage.getIgnoredLabelCollisionCount()).isEqualTo(1);
+    }
+
+    @Test
+    void 
managedLabelCollisionsAreSkippedWithoutDroppingTheBatch(CapturedOutput output) {
+        when(victoriaMetricsProperties.insert()).thenReturn(new 
VictoriaMetricsProperties.InsertConfig(
+                1, Integer.MAX_VALUE, new 
VictoriaMetricsProperties.Compression(false)));
+        CollectRep.MetricsData metricsData = generateMockedMetricsData();
+        when(metricsData.getLabels()).thenReturn(Map.of(
+                "__name__", "custom-name",
+                "__monitor_id__", "custom-monitor"));
+        victoriaMetricsDataStorage = new 
VictoriaMetricsDataStorage(victoriaMetricsProperties, restTemplate);
+
+        victoriaMetricsDataStorage.saveData(metricsData);
+
+        Awaitility.await()
+                .atMost(5, TimeUnit.SECONDS)
+                .untilAsserted(() -> 
assertThat(postForEntityCount.get()).isEqualTo(1));
+        VictoriaMetricsDataStorage.VictoriaMetricsContent content =
+                JsonUtil.fromJson(lastPayload.get().trim(), 
VictoriaMetricsDataStorage.VictoriaMetricsContent.class);
+        assertThat(content.getMetric())
+                .containsEntry("__monitor_id__", "0")
+                .doesNotContainEntry("__name__", "custom-name")
+                .doesNotContainEntry("__monitor_id__", "custom-monitor");
+        
assertThat(victoriaMetricsDataStorage.getIgnoredLabelCollisionCount()).isEqualTo(2);
+        assertThat(output.getAll())
+                .contains("__name__")
+                .contains("__monitor_id__")
+                .doesNotContain("custom-name")
+                .doesNotContain("custom-monitor");
+    }
+
+    @Test
+    void clusterWriterUsesTheSameNonDestructiveCollisionPolicy() {
+        when(responseEntity.getStatusCode()).thenReturn(HttpStatus.OK);
+        when(responseEntity.getBody()).thenReturn("{\"status\":\"success\"}");
+        when(restTemplate.postForEntity(
+                startsWith("http://vm-insert";),
+                any(HttpEntity.class),
+                eq(String.class)))
+                .thenAnswer(invocation -> {
+                    HttpEntity<?> httpEntity = invocation.getArgument(1);
+                    lastPayload.set((String) httpEntity.getBody());
+                    return responseEntity;
+                });
+        VictoriaMetricsClusterProperties clusterProperties = new 
VictoriaMetricsClusterProperties(
+                true,
+                "0",
+                new VictoriaMetricsInsertProperties("http://vm-insert";, null, 
null, 10, 0),
+                new VictoriaMetricsSelectProperties("http://vm-select";, null, 
null));
+        CollectRep.MetricsData metricsData = generateMockedMetricsData();
+        when(metricsData.getLabels()).thenReturn(Map.of(
+                "instance", "custom-instance",
+                "region", "west"));
+        VictoriaMetricsClusterDataStorage clusterStorage =
+                new VictoriaMetricsClusterDataStorage(clusterProperties, 
restTemplate);
+
+        try {
+            clusterStorage.saveData(metricsData);
+
+            VictoriaMetricsDataStorage.VictoriaMetricsContent content = 
JsonUtil.fromJson(
+                    lastPayload.get().trim(), 
VictoriaMetricsDataStorage.VictoriaMetricsContent.class);
+            assertThat(content.getMetric())
+                    .containsEntry("instance", "storage-instance")
+                    .containsEntry("region", "west");
+            
assertThat(clusterStorage.getIgnoredLabelCollisionCount()).isEqualTo(1);
+        } finally {
+            clusterStorage.destroy();
+        }
+    }
+
     @AfterEach
     void stop() {
         if (victoriaMetricsDataStorage != null) {
@@ -200,6 +303,8 @@ class VictoriaMetricsDataStorageTest {
         when(mockMetricsData.getTime()).thenReturn(System.currentTimeMillis());
         when(mockMetricsData.getCode()).thenReturn(CollectRep.Code.SUCCESS);
         when(mockMetricsData.getApp()).thenReturn("app");
+        when(mockMetricsData.getInstance()).thenReturn("storage-instance");
+        when(mockMetricsData.getLabels()).thenReturn(Map.of());
 
         CollectRep.ValueRow mockValueRow = 
Mockito.mock(CollectRep.ValueRow.class);
         List<String> columnValues = List.of("server-test-01", "68.7");
diff --git a/home/docs/start/victoria-metrics-init.md 
b/home/docs/start/victoria-metrics-init.md
index eb0f3265a9..3754686f71 100644
--- a/home/docs/start/victoria-metrics-init.md
+++ b/home/docs/start/victoria-metrics-init.md
@@ -148,6 +148,25 @@ warehouse:
 
 Once configured, restart HertzBeat to connect to the VictoriaMetrics cluster.
 
+### Custom Label Collision Policy
+
+Monitor custom labels keep their existing Prometheus semantics when HertzBeat
+writes to VictoriaMetrics:
+
+- `job` and ordinary custom labels continue to use their configured values.
+- `instance`, `__name__`, `__monitor_id__`, `__metrics__`, and `__metric__` are
+  managed by HertzBeat. If a monitor supplies one of these custom-label keys,
+  HertzBeat ignores only the conflicting label, stores the remaining metrics,
+  and reports the key name through a rate-limited warning and cumulative count.
+  Label values are not written to the warning.
+
+Before upgrading, inspect monitor custom labels and rename managed keys. If an
+existing monitor uses a custom `instance` value for a separate identity, move
+that value to a distinct label such as `target_instance`; new samples use the
+HertzBeat monitor instance. Existing VictoriaMetrics series are not rewritten.
+No migration is needed for monitors that use `job` or other ordinary custom
+labels.
+
 ### FAQ
 
 1. Do both the time series databases need to be configured? Can they both be 
used?


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

Reply via email to