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 e635f50580 Support log contains filters
e635f50580 is described below

commit e635f505804f994c51c4707a93dca619fac9df5e
Author: Logic <[email protected]>
AuthorDate: Tue Jun 9 21:23:45 2026 +0800

    Support log contains filters
---
 .../logs/service/impl/LogQueryServiceImpl.java     | 61 ++++++++++++++++++--
 .../logs/controller/LogQueryControllerTest.java    | 66 ++++++++++++++++++++++
 2 files changed, 122 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 f91688518b..b1da8541a4 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
@@ -28,6 +28,7 @@ import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -72,10 +73,15 @@ public class LogQueryServiceImpl implements LogQueryService 
{
     private static final String LOG_FILTER_NEGATION_PREFIX = "!";
     private static final String LOG_FILTER_IN_PREFIX = "__hz_in__:";
     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_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*$",
             Pattern.CASE_INSENSITIVE);
+    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 Set<String> WORKSPACE_INFRA_SERVICE_NAMES = Set.of(
             "otelcol-contrib",
@@ -1205,7 +1211,7 @@ public class LogQueryServiceImpl implements 
LogQueryService {
     }
 
     private boolean hasComplexAttributeFilterValues(Map<String, String> 
filters) {
-        return filters != null && 
filters.values().stream().anyMatch(this::isListLogAttributeFilter);
+        return filters != null && 
filters.values().stream().anyMatch(this::isComplexLogAttributeFilter);
     }
 
     private Map<String, String> parseLogAttributeFilter(String 
filterExpression) {
@@ -1221,7 +1227,8 @@ public class LogQueryServiceImpl implements 
LogQueryService {
             if (!StringUtils.hasText(token)) {
                 continue;
             }
-            if (allowListOperators && appendLogFilterListValues(filters, 
token)) {
+            if (allowListOperators && (appendLogFilterListValues(filters, 
token)
+                    || appendLogFilterTextValue(filters, token))) {
                 continue;
             }
             boolean negate = false;
@@ -1272,6 +1279,24 @@ public class LogQueryServiceImpl implements 
LogQueryService {
         return true;
     }
 
+    private boolean appendLogFilterTextValue(Map<String, String> filters, 
String token) {
+        Matcher matcher = LOG_FILTER_TEXT_OPERATOR_PATTERN.matcher(token);
+        if (!matcher.matches()) {
+            return false;
+        }
+        String key = matcher.group(1).trim();
+        String operator = matcher.group(2).trim().replaceAll("\\s+", " ");
+        String value = stripFilterQuotes(matcher.group(3).trim());
+        if (!isSafeAttributeKey(key) || !StringUtils.hasText(value)) {
+            return false;
+        }
+        String prefix = "not contains".equalsIgnoreCase(operator)
+                ? LOG_FILTER_NOT_CONTAINS_PREFIX
+                : LOG_FILTER_CONTAINS_PREFIX;
+        filters.put(key, prefix + value);
+        return true;
+    }
+
     private List<String> splitLogFilterClauses(String filterExpression) {
         List<String> clauses = new ArrayList<>();
         StringBuilder current = new StringBuilder();
@@ -1449,6 +1474,14 @@ public class LogQueryServiceImpl implements 
LogQueryService {
             return 
splitListLogAttributeValues(expectedValue.substring(LOG_FILTER_NOT_IN_PREFIX.length())).stream()
                     .noneMatch(expected -> 
matchesOptionalResourceValue(actualValue, expected));
         }
+        if (isContainsLogAttributeFilter(expectedValue)) {
+            return matchesContainedResourceValue(actualValue,
+                    
expectedValue.substring(LOG_FILTER_CONTAINS_PREFIX.length()));
+        }
+        if (isNotContainsLogAttributeFilter(expectedValue)) {
+            return !matchesContainedResourceValue(actualValue,
+                    
expectedValue.substring(LOG_FILTER_NOT_CONTAINS_PREFIX.length()));
+        }
         if (isNegatedLogAttributeFilter(expectedValue)) {
             return !matchesOptionalResourceValue(actualValue, 
expectedValue.substring(LOG_FILTER_NEGATION_PREFIX.length()));
         }
@@ -1460,11 +1493,13 @@ public class LogQueryServiceImpl implements 
LogQueryService {
     }
 
     private boolean isExclusionLogAttributeFilter(String expectedValue) {
-        return isNegatedLogAttributeFilter(expectedValue) || 
isNotInLogAttributeFilter(expectedValue);
+        return isNegatedLogAttributeFilter(expectedValue) || 
isNotInLogAttributeFilter(expectedValue)
+                || isNotContainsLogAttributeFilter(expectedValue);
     }
 
-    private boolean isListLogAttributeFilter(String expectedValue) {
-        return isInLogAttributeFilter(expectedValue) || 
isNotInLogAttributeFilter(expectedValue);
+    private boolean isComplexLogAttributeFilter(String expectedValue) {
+        return isInLogAttributeFilter(expectedValue) || 
isNotInLogAttributeFilter(expectedValue)
+                || isContainsLogAttributeFilter(expectedValue) || 
isNotContainsLogAttributeFilter(expectedValue);
     }
 
     private boolean isInLogAttributeFilter(String expectedValue) {
@@ -1475,6 +1510,14 @@ public class LogQueryServiceImpl implements 
LogQueryService {
         return expectedValue != null && 
expectedValue.startsWith(LOG_FILTER_NOT_IN_PREFIX);
     }
 
+    private boolean isContainsLogAttributeFilter(String expectedValue) {
+        return expectedValue != null && 
expectedValue.startsWith(LOG_FILTER_CONTAINS_PREFIX);
+    }
+
+    private boolean isNotContainsLogAttributeFilter(String expectedValue) {
+        return expectedValue != null && 
expectedValue.startsWith(LOG_FILTER_NOT_CONTAINS_PREFIX);
+    }
+
     private List<String> splitListLogAttributeValues(String encodedValues) {
         if (!StringUtils.hasText(encodedValues)) {
             return List.of();
@@ -1512,6 +1555,14 @@ public class LogQueryServiceImpl implements 
LogQueryService {
                 && actualValue.equalsIgnoreCase(expectedValue.trim());
     }
 
+    private boolean matchesContainedResourceValue(String actualValue, String 
expectedValue) {
+        if (!StringUtils.hasText(expectedValue)) {
+            return true;
+        }
+        return StringUtils.hasText(actualValue)
+                && 
actualValue.toLowerCase(Locale.ROOT).contains(expectedValue.trim().toLowerCase(Locale.ROOT));
+    }
+
     private boolean hasWorkspaceContext() {
         return 
StringUtils.hasText(AuthTokenRequestContext.currentWorkspaceId());
     }
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 d6a6296267..a281d72b23 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
@@ -472,6 +472,72 @@ class LogQueryControllerTest {
                 any(), any(), any(), anySet(), eq(false));
     }
 
+    @Test
+    void 
testOverviewStatsAppliesContainsAndNotContainsFiltersWithRowFallback() throws 
Exception {
+        LogEntry infoLog = LogEntry.builder()
+                .timeUnixNano(1734005477630000000L)
+                .severityNumber(9)
+                .severityText("INFO")
+                .body("stable checkout info")
+                .resource(new HashMap<>(Map.of(
+                        "service.name", "checkout",
+                        "service.version", "1.2.3",
+                        "host.name", "checkout-1")))
+                .attributes(new HashMap<>(Map.of("http.route", 
"/api/checkout")))
+                .build();
+        LogEntry errorLog = LogEntry.builder()
+                .timeUnixNano(1734005477640000000L)
+                .severityNumber(17)
+                .severityText("ERROR")
+                .body("stable checkout error")
+                .resource(new HashMap<>(Map.of(
+                        "service.name", "checkout",
+                        "service.version", "1.2.4",
+                        "host.name", "checkout-2")))
+                .attributes(new HashMap<>(Map.of("http.route", 
"/api/checkout/payment")))
+                .build();
+        LogEntry canaryLog = LogEntry.builder()
+                .timeUnixNano(1734005477650000000L)
+                .severityNumber(17)
+                .severityText("ERROR")
+                .body("canary checkout error")
+                .resource(new HashMap<>(Map.of(
+                        "service.name", "checkout",
+                        "service.version", "1.2.3-canary",
+                        "host.name", "checkout-canary")))
+                .attributes(new HashMap<>(Map.of("http.route", 
"/api/checkout")))
+                .build();
+        LogEntry cartLog = LogEntry.builder()
+                .timeUnixNano(1734005477660000000L)
+                .severityNumber(13)
+                .severityText("WARN")
+                .body("cart warn")
+                .resource(new HashMap<>(Map.of(
+                        "service.name", "checkout",
+                        "service.version", "1.2.4",
+                        "host.name", "checkout-3")))
+                .attributes(new HashMap<>(Map.of("http.route", "/api/cart")))
+                .build();
+        when(historyDataReader.queryLogsByMultipleConditions(any(), any(), 
any(),
+                any(), any(), any(), any())).thenReturn(List.of(infoLog, 
errorLog, canaryLog, cartLog));
+
+        mockMvc.perform(MockMvcRequestBuilders.get("/api/logs/stats/overview")
+                        .param("resourceFilter", "service.version CONTAINS 
'1.2' "
+                                + "and host.name NOT CONTAINS 'canary'")
+                        .param("attributeFilter", "http.route CONTAINS 
'checkout'"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value((int) 
CommonConstants.SUCCESS_CODE))
+                .andExpect(jsonPath("$.data.totalCount").value(2))
+                .andExpect(jsonPath("$.data.infoCount").value(1))
+                .andExpect(jsonPath("$.data.errorCount").value(1))
+                .andExpect(jsonPath("$.data.warnCount").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