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

zqr10159 pushed a commit to branch 2.0.0
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git


The following commit(s) were added to refs/heads/2.0.0 by this push:
     new ea6a637847 Support log exists filters
ea6a637847 is described below

commit ea6a637847e82d178564948c436c1291bdc73126
Author: Logic <[email protected]>
AuthorDate: Tue Jun 9 21:28:25 2026 +0800

    Support log exists filters
---
 .../logs/service/impl/LogQueryServiceImpl.java     | 48 +++++++++++++++++---
 .../logs/controller/LogQueryControllerTest.java    | 51 ++++++++++++++++++++++
 2 files changed, 94 insertions(+), 5 deletions(-)

diff --git 
a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java
 
b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java
index b1da8541a4..17367d277a 100644
--- 
a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java
+++ 
b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/logs/service/impl/LogQueryServiceImpl.java
@@ -75,6 +75,8 @@ public class LogQueryServiceImpl implements LogQueryService {
     private static final String LOG_FILTER_NOT_IN_PREFIX = "__hz_not_in__:";
     private static final String LOG_FILTER_CONTAINS_PREFIX = 
"__hz_contains__:";
     private static final String LOG_FILTER_NOT_CONTAINS_PREFIX = 
"__hz_not_contains__:";
+    private static final String LOG_FILTER_EXISTS_PREFIX = "__hz_exists__";
+    private static final String LOG_FILTER_NOT_EXISTS_PREFIX = 
"__hz_not_exists__";
     private static final String LOG_FILTER_VALUE_DELIMITER = "\u001F";
     private static final Pattern LOG_FILTER_LIST_OPERATOR_PATTERN = 
Pattern.compile(
             "^\\s*([A-Za-z0-9._:-]+)\\s+(NOT\\s+IN|IN)\\s*(\\(.+\\))\\s*$",
@@ -82,6 +84,9 @@ public class LogQueryServiceImpl implements LogQueryService {
     private static final Pattern LOG_FILTER_TEXT_OPERATOR_PATTERN = 
Pattern.compile(
             
"^\\s*([A-Za-z0-9._:-]+)\\s+(NOT\\s+CONTAINS|CONTAINS)\\s+(.+)\\s*$",
             Pattern.CASE_INSENSITIVE);
+    private static final Pattern LOG_FILTER_PRESENCE_OPERATOR_PATTERN = 
Pattern.compile(
+            "^\\s*([A-Za-z0-9._:-]+)\\s+(NOT\\s+EXISTS|EXISTS)\\s*$",
+            Pattern.CASE_INSENSITIVE);
 
     private static final Set<String> WORKSPACE_INFRA_SERVICE_NAMES = Set.of(
             "otelcol-contrib",
@@ -1228,7 +1233,8 @@ public class LogQueryServiceImpl implements 
LogQueryService {
                 continue;
             }
             if (allowListOperators && (appendLogFilterListValues(filters, 
token)
-                    || appendLogFilterTextValue(filters, token))) {
+                    || appendLogFilterTextValue(filters, token)
+                    || appendLogFilterPresenceValue(filters, token))) {
                 continue;
             }
             boolean negate = false;
@@ -1297,6 +1303,22 @@ public class LogQueryServiceImpl implements 
LogQueryService {
         return true;
     }
 
+    private boolean appendLogFilterPresenceValue(Map<String, String> filters, 
String token) {
+        Matcher matcher = LOG_FILTER_PRESENCE_OPERATOR_PATTERN.matcher(token);
+        if (!matcher.matches()) {
+            return false;
+        }
+        String key = matcher.group(1).trim();
+        String operator = matcher.group(2).trim().replaceAll("\\s+", " ");
+        if (!isSafeAttributeKey(key)) {
+            return false;
+        }
+        filters.put(key, "not exists".equalsIgnoreCase(operator)
+                ? LOG_FILTER_NOT_EXISTS_PREFIX
+                : LOG_FILTER_EXISTS_PREFIX);
+        return true;
+    }
+
     private List<String> splitLogFilterClauses(String filterExpression) {
         List<String> clauses = new ArrayList<>();
         StringBuilder current = new StringBuilder();
@@ -1462,10 +1484,17 @@ public class LogQueryServiceImpl implements 
LogQueryService {
             return 
expectedAttributes.values().stream().allMatch(this::isExclusionLogAttributeFilter);
         }
         return expectedAttributes.entrySet().stream()
-                .allMatch(entry -> 
matchesAttributeFilter(resolveMapValue(source, entry.getKey()), 
entry.getValue()));
+                .allMatch(entry -> 
matchesAttributeFilter(resolveMapValue(source, entry.getKey()), 
entry.getValue(),
+                        source.containsKey(entry.getKey())));
     }
 
-    private boolean matchesAttributeFilter(String actualValue, String 
expectedValue) {
+    private boolean matchesAttributeFilter(String actualValue, String 
expectedValue, boolean keyExists) {
+        if (isExistsLogAttributeFilter(expectedValue)) {
+            return keyExists;
+        }
+        if (isNotExistsLogAttributeFilter(expectedValue)) {
+            return !keyExists;
+        }
         if (isInLogAttributeFilter(expectedValue)) {
             return 
splitListLogAttributeValues(expectedValue.substring(LOG_FILTER_IN_PREFIX.length())).stream()
                     .anyMatch(expected -> 
matchesOptionalResourceValue(actualValue, expected));
@@ -1494,12 +1523,13 @@ public class LogQueryServiceImpl implements 
LogQueryService {
 
     private boolean isExclusionLogAttributeFilter(String expectedValue) {
         return isNegatedLogAttributeFilter(expectedValue) || 
isNotInLogAttributeFilter(expectedValue)
-                || isNotContainsLogAttributeFilter(expectedValue);
+                || isNotContainsLogAttributeFilter(expectedValue) || 
isNotExistsLogAttributeFilter(expectedValue);
     }
 
     private boolean isComplexLogAttributeFilter(String expectedValue) {
         return isInLogAttributeFilter(expectedValue) || 
isNotInLogAttributeFilter(expectedValue)
-                || isContainsLogAttributeFilter(expectedValue) || 
isNotContainsLogAttributeFilter(expectedValue);
+                || isContainsLogAttributeFilter(expectedValue) || 
isNotContainsLogAttributeFilter(expectedValue)
+                || isExistsLogAttributeFilter(expectedValue) || 
isNotExistsLogAttributeFilter(expectedValue);
     }
 
     private boolean isInLogAttributeFilter(String expectedValue) {
@@ -1518,6 +1548,14 @@ public class LogQueryServiceImpl implements 
LogQueryService {
         return expectedValue != null && 
expectedValue.startsWith(LOG_FILTER_NOT_CONTAINS_PREFIX);
     }
 
+    private boolean isExistsLogAttributeFilter(String expectedValue) {
+        return LOG_FILTER_EXISTS_PREFIX.equals(expectedValue);
+    }
+
+    private boolean isNotExistsLogAttributeFilter(String expectedValue) {
+        return LOG_FILTER_NOT_EXISTS_PREFIX.equals(expectedValue);
+    }
+
     private List<String> splitListLogAttributeValues(String encodedValues) {
         if (!StringUtils.hasText(encodedValues)) {
             return List.of();
diff --git 
a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/logs/controller/LogQueryControllerTest.java
 
b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/logs/controller/LogQueryControllerTest.java
index a281d72b23..e96aeb7fc3 100644
--- 
a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/logs/controller/LogQueryControllerTest.java
+++ 
b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/logs/controller/LogQueryControllerTest.java
@@ -538,6 +538,57 @@ class LogQueryControllerTest {
                 any(), any(), any(), anySet(), eq(false));
     }
 
+    @Test
+    void testOverviewStatsAppliesExistsAndNotExistsFiltersWithRowFallback() 
throws Exception {
+        LogEntry matchingLog = LogEntry.builder()
+                .timeUnixNano(1734005477630000000L)
+                .severityNumber(17)
+                .severityText("ERROR")
+                .body("checkout error with typed failure")
+                .resource(new HashMap<>(Map.of(
+                        "service.name", "checkout",
+                        "service.version", "1.2.3")))
+                .attributes(new HashMap<>(Map.of("error.type", 
"payment.failure")))
+                .build();
+        LogEntry missingErrorTypeLog = LogEntry.builder()
+                .timeUnixNano(1734005477640000000L)
+                .severityNumber(9)
+                .severityText("INFO")
+                .body("checkout info without error type")
+                .resource(new HashMap<>(Map.of(
+                        "service.name", "checkout",
+                        "service.version", "1.2.4")))
+                .attributes(new HashMap<>(Map.of("http.route", 
"/api/checkout")))
+                .build();
+        LogEntry environmentLog = LogEntry.builder()
+                .timeUnixNano(1734005477650000000L)
+                .severityNumber(17)
+                .severityText("ERROR")
+                .body("checkout error with environment")
+                .resource(new HashMap<>(Map.of(
+                        "service.name", "checkout",
+                        "service.version", "1.2.5",
+                        "deployment.environment.name", "prod")))
+                .attributes(new HashMap<>(Map.of("error.type", 
"payment.failure")))
+                .build();
+        when(historyDataReader.queryLogsByMultipleConditions(any(), any(), 
any(),
+                any(), any(), any(), any())).thenReturn(List.of(matchingLog, 
missingErrorTypeLog, environmentLog));
+
+        mockMvc.perform(MockMvcRequestBuilders.get("/api/logs/stats/overview")
+                        .param("resourceFilter", "deployment.environment.name 
NOT EXISTS")
+                        .param("attributeFilter", "error.type EXISTS"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value((int) 
CommonConstants.SUCCESS_CODE))
+                .andExpect(jsonPath("$.data.totalCount").value(1))
+                .andExpect(jsonPath("$.data.errorCount").value(1))
+                .andExpect(jsonPath("$.data.infoCount").value(0));
+
+        verify(historyDataReader).queryLogsByMultipleConditions(any(), any(), 
any(),
+                any(), any(), any(), any());
+        verify(historyDataReader, never()).countLogsBySeverityBuckets(any(), 
any(), any(), any(),
+                any(), any(), any(), anySet(), eq(false));
+    }
+
     @Test
     void testLogContextPrefersEntityIdentityOverConflictingRouteContext() 
throws Exception {
         ObservabilityWorkspaceQueryGateway workspaceQueryGateway = 
org.mockito.Mockito.mock(ObservabilityWorkspaceQueryGateway.class);


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

Reply via email to