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 499130500b Support log list IN filters
499130500b is described below

commit 499130500b64567e9d662453919d54fbb80f1313
Author: Logic <[email protected]>
AuthorDate: Tue Jun 9 20:56:40 2026 +0800

    Support log list IN filters
---
 .../logs/service/impl/LogQueryServiceImpl.java     | 185 ++++++++++++++++++++-
 .../logs/controller/LogQueryControllerTest.java    |  63 +++++++
 2 files changed, 241 insertions(+), 7 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 66369ae671..315782d62d 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
@@ -21,6 +21,7 @@ import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -31,6 +32,8 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
@@ -67,6 +70,12 @@ public class LogQueryServiceImpl implements LogQueryService {
     private static final int MAX_CONTEXT_LIMIT = 50;
     private static final long DEFAULT_CONTEXT_WINDOW_MS = 300_000L;
     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_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 Set<String> WORKSPACE_INFRA_SERVICE_NAMES = Set.of(
             "otelcol-contrib",
@@ -128,8 +137,8 @@ public class LogQueryServiceImpl implements LogQueryService 
{
                                String serviceName, String serviceNamespace, 
String environment,
                                String resourceFilter, String attributeFilter,
                                Integer pageIndex, Integer pageSize, boolean 
hideInternal, boolean hideNoise) {
-        Map<String, String> resourceFilters = 
parseLogAttributeFilter(resourceFilter);
-        Map<String, String> attributeFilters = 
parseLogAttributeFilter(attributeFilter);
+        Map<String, String> resourceFilters = 
parseLogAttributeFilter(resourceFilter, true);
+        Map<String, String> attributeFilters = 
parseLogAttributeFilter(attributeFilter, true);
         return getPagedLogs(start, end, traceId, spanId, severityNumber, 
severityText, search,
                 serviceName, serviceNamespace, environment, resourceFilters, 
attributeFilters,
                 pageIndex, pageSize, hideInternal, hideNoise);
@@ -143,8 +152,8 @@ public class LogQueryServiceImpl implements LogQueryService 
{
                                Integer pageIndex, Integer pageSize, boolean 
hideInternal, boolean hideNoise) {
         LogServiceContext context = 
resolveEntityFirstLogServiceContext(entityId, serviceName, serviceNamespace, 
environment);
         Map<String, String> resourceFilters = removeEntityScopeResourceFilters(
-                context, parseLogAttributeFilter(resourceFilter));
-        Map<String, String> attributeFilters = 
parseLogAttributeFilter(attributeFilter);
+                context, parseLogAttributeFilter(resourceFilter, true));
+        Map<String, String> attributeFilters = 
parseLogAttributeFilter(attributeFilter, true);
         return getPagedLogs(start, end, traceId, spanId, severityNumber, 
severityText, search,
                 context.serviceName(), context.serviceNamespace(), 
context.environment(), resourceFilters, attributeFilters,
                 pageIndex, pageSize, hideInternal, hideNoise);
@@ -831,6 +840,12 @@ public class LogQueryServiceImpl implements 
LogQueryService {
         Sort sort = Sort.by(Sort.Direction.DESC, "timeUnixNano");
         PageRequest pageRequest = PageRequest.of(resolvedPageIndex, 
resolvedPageSize, sort);
 
+        if (hasComplexAttributeFilters(resourceFilters, attributeFilters)) {
+            return getRowFilteredPagedLogs(start, end, traceId, spanId, 
severityNumber, severityText, search,
+                    serviceName, serviceNamespace, environment, 
resourceFilters, attributeFilters,
+                    pageRequest, offset, resolvedPageSize, hideInternal, 
hideNoise);
+        }
+
         if (hasWorkspaceContext()) {
             return getWorkspacePagedLogs(start, end, traceId, spanId, 
severityNumber, severityText, search,
                     serviceName, serviceNamespace, environment, 
resourceFilters, attributeFilters,
@@ -1169,16 +1184,30 @@ public class LogQueryServiceImpl implements 
LogQueryService {
                 || (attributeFilters != null && !attributeFilters.isEmpty());
     }
 
+    private boolean hasComplexAttributeFilters(Map<String, String> 
resourceFilters, Map<String, String> attributeFilters) {
+        return hasComplexAttributeFilterValues(resourceFilters) || 
hasComplexAttributeFilterValues(attributeFilters);
+    }
+
+    private boolean hasComplexAttributeFilterValues(Map<String, String> 
filters) {
+        return filters != null && 
filters.values().stream().anyMatch(this::isListLogAttributeFilter);
+    }
+
     private Map<String, String> parseLogAttributeFilter(String 
filterExpression) {
+        return parseLogAttributeFilter(filterExpression, false);
+    }
+
+    private Map<String, String> parseLogAttributeFilter(String 
filterExpression, boolean allowListOperators) {
         if (!StringUtils.hasText(filterExpression)) {
             return Collections.emptyMap();
         }
         Map<String, String> filters = new HashMap<>();
-        String[] tokens = 
filterExpression.trim().split("(?i)\\s+and\\s+|\\s*,\\s*");
-        for (String token : tokens) {
+        for (String token : splitLogFilterClauses(filterExpression)) {
             if (!StringUtils.hasText(token)) {
                 continue;
             }
+            if (allowListOperators && appendLogFilterListValues(filters, 
token)) {
+                continue;
+            }
             boolean negate = false;
             int separatorIndex = token.indexOf("!=");
             if (separatorIndex >= 0) {
@@ -1202,6 +1231,115 @@ public class LogQueryServiceImpl implements 
LogQueryService {
         return filters.isEmpty() ? Collections.emptyMap() : 
Map.copyOf(filters);
     }
 
+    private boolean appendLogFilterListValues(Map<String, String> filters, 
String token) {
+        Matcher matcher = LOG_FILTER_LIST_OPERATOR_PATTERN.matcher(token);
+        if (!matcher.matches()) {
+            return false;
+        }
+        String key = matcher.group(1).trim();
+        String operator = matcher.group(2).trim().replaceAll("\\s+", " ");
+        String valueList = matcher.group(3).trim();
+        if (!isSafeAttributeKey(key) || valueList.length() < 2
+                || !valueList.startsWith("(") || !valueList.endsWith(")")) {
+            return false;
+        }
+        List<String> values = splitLogFilterListValues(valueList.substring(1, 
valueList.length() - 1)).stream()
+                .map(value -> stripFilterQuotes(value.trim()))
+                .filter(StringUtils::hasText)
+                .distinct()
+                .toList();
+        if (values.isEmpty()) {
+            return false;
+        }
+        String prefix = "not in".equalsIgnoreCase(operator) ? 
LOG_FILTER_NOT_IN_PREFIX : LOG_FILTER_IN_PREFIX;
+        filters.put(key, prefix + String.join(LOG_FILTER_VALUE_DELIMITER, 
values));
+        return true;
+    }
+
+    private List<String> splitLogFilterClauses(String filterExpression) {
+        List<String> clauses = new ArrayList<>();
+        StringBuilder current = new StringBuilder();
+        int depth = 0;
+        char quote = 0;
+        for (int index = 0; index < filterExpression.length(); index++) {
+            char character = filterExpression.charAt(index);
+            if (quote != 0) {
+                current.append(character);
+                if (character == quote) {
+                    quote = 0;
+                }
+                continue;
+            }
+            if (character == '\'' || character == '"') {
+                quote = character;
+                current.append(character);
+                continue;
+            }
+            if (character == '(') {
+                depth++;
+                current.append(character);
+                continue;
+            }
+            if (character == ')') {
+                depth = Math.max(0, depth - 1);
+                current.append(character);
+                continue;
+            }
+            if (depth == 0 && character == ',') {
+                addLogFilterClause(clauses, current);
+                continue;
+            }
+            if (depth == 0 && isLogFilterAndDelimiter(filterExpression, 
index)) {
+                addLogFilterClause(clauses, current);
+                index += 4;
+                continue;
+            }
+            current.append(character);
+        }
+        addLogFilterClause(clauses, current);
+        return clauses;
+    }
+
+    private List<String> splitLogFilterListValues(String values) {
+        List<String> result = new ArrayList<>();
+        StringBuilder current = new StringBuilder();
+        char quote = 0;
+        for (int index = 0; index < values.length(); index++) {
+            char character = values.charAt(index);
+            if (quote != 0) {
+                current.append(character);
+                if (character == quote) {
+                    quote = 0;
+                }
+                continue;
+            }
+            if (character == '\'' || character == '"') {
+                quote = character;
+                current.append(character);
+                continue;
+            }
+            if (character == ',') {
+                addLogFilterClause(result, current);
+                continue;
+            }
+            current.append(character);
+        }
+        addLogFilterClause(result, current);
+        return result;
+    }
+
+    private void addLogFilterClause(List<String> clauses, StringBuilder 
current) {
+        String clause = current.toString().trim();
+        if (StringUtils.hasText(clause)) {
+            clauses.add(clause);
+        }
+        current.setLength(0);
+    }
+
+    private boolean isLogFilterAndDelimiter(String value, int index) {
+        return index + 5 <= value.length() && value.regionMatches(true, index, 
" and ", 0, 5);
+    }
+
     private Map<String, String> 
removeEntityScopeResourceFilters(LogServiceContext context,
                                                                  Map<String, 
String> resourceFilters) {
         if (context == null || resourceFilters == null || 
resourceFilters.isEmpty()) {
@@ -1280,13 +1418,21 @@ public class LogQueryServiceImpl implements 
LogQueryService {
             return true;
         }
         if (source == null || source.isEmpty()) {
-            return 
expectedAttributes.values().stream().allMatch(this::isNegatedLogAttributeFilter);
+            return 
expectedAttributes.values().stream().allMatch(this::isExclusionLogAttributeFilter);
         }
         return expectedAttributes.entrySet().stream()
                 .allMatch(entry -> 
matchesAttributeFilter(resolveMapValue(source, entry.getKey()), 
entry.getValue()));
     }
 
     private boolean matchesAttributeFilter(String actualValue, String 
expectedValue) {
+        if (isInLogAttributeFilter(expectedValue)) {
+            return 
splitListLogAttributeValues(expectedValue.substring(LOG_FILTER_IN_PREFIX.length())).stream()
+                    .anyMatch(expected -> 
matchesOptionalResourceValue(actualValue, expected));
+        }
+        if (isNotInLogAttributeFilter(expectedValue)) {
+            return 
splitListLogAttributeValues(expectedValue.substring(LOG_FILTER_NOT_IN_PREFIX.length())).stream()
+                    .noneMatch(expected -> 
matchesOptionalResourceValue(actualValue, expected));
+        }
         if (isNegatedLogAttributeFilter(expectedValue)) {
             return !matchesOptionalResourceValue(actualValue, 
expectedValue.substring(LOG_FILTER_NEGATION_PREFIX.length()));
         }
@@ -1297,6 +1443,31 @@ public class LogQueryServiceImpl implements 
LogQueryService {
         return expectedValue != null && 
expectedValue.startsWith(LOG_FILTER_NEGATION_PREFIX);
     }
 
+    private boolean isExclusionLogAttributeFilter(String expectedValue) {
+        return isNegatedLogAttributeFilter(expectedValue) || 
isNotInLogAttributeFilter(expectedValue);
+    }
+
+    private boolean isListLogAttributeFilter(String expectedValue) {
+        return isInLogAttributeFilter(expectedValue) || 
isNotInLogAttributeFilter(expectedValue);
+    }
+
+    private boolean isInLogAttributeFilter(String expectedValue) {
+        return expectedValue != null && 
expectedValue.startsWith(LOG_FILTER_IN_PREFIX);
+    }
+
+    private boolean isNotInLogAttributeFilter(String expectedValue) {
+        return expectedValue != null && 
expectedValue.startsWith(LOG_FILTER_NOT_IN_PREFIX);
+    }
+
+    private List<String> splitListLogAttributeValues(String encodedValues) {
+        if (!StringUtils.hasText(encodedValues)) {
+            return List.of();
+        }
+        return 
List.of(encodedValues.split(Pattern.quote(LOG_FILTER_VALUE_DELIMITER), 
-1)).stream()
+                .filter(StringUtils::hasText)
+                .toList();
+    }
+
     private boolean shouldHideWorkspaceLog(LogEntry logEntry, boolean 
hideInternal, boolean hideNoise) {
         String serviceName = resolveServiceName(logEntry);
         if (!StringUtils.hasText(serviceName)) {
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 cede67c0f2..8cf11ff05e 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
@@ -719,6 +719,69 @@ class LogQueryControllerTest {
                 eq(Map.of("service.version", "!1.2.3")), 
eq(Map.of("http.route", "!/checkout")));
     }
 
+    @Test
+    void testListLogsAppliesInAndNotInFiltersWithRowFallback() throws 
Exception {
+        LogEntry stableLog = LogEntry.builder()
+                .timeUnixNano(1734005477630000000L)
+                .severityText("INFO")
+                .body("stable checkout log")
+                .resource(new HashMap<>(java.util.Map.of(
+                        "service.name", "checkout",
+                        "service.version", "1.2.3",
+                        "host.name", "checkout-1")))
+                .attributes(new HashMap<>(java.util.Map.of("http.route", 
"/checkout")))
+                .build();
+        LogEntry canaryLog = LogEntry.builder()
+                .timeUnixNano(1734005477640000000L)
+                .severityText("INFO")
+                .body("canary checkout log")
+                .resource(new HashMap<>(java.util.Map.of(
+                        "service.name", "checkout",
+                        "service.version", "1.2.3",
+                        "host.name", "checkout-canary")))
+                .attributes(new HashMap<>(java.util.Map.of("http.route", 
"/checkout")))
+                .build();
+        LogEntry cartLog = LogEntry.builder()
+                .timeUnixNano(1734005477650000000L)
+                .severityText("INFO")
+                .body("cart checkout log")
+                .resource(new HashMap<>(java.util.Map.of(
+                        "service.name", "checkout",
+                        "service.version", "1.2.4",
+                        "host.name", "checkout-2")))
+                .attributes(new HashMap<>(java.util.Map.of("http.route", 
"/cart")))
+                .build();
+        LogEntry otherVersionLog = LogEntry.builder()
+                .timeUnixNano(1734005477660000000L)
+                .severityText("INFO")
+                .body("other checkout log")
+                .resource(new HashMap<>(java.util.Map.of(
+                        "service.name", "checkout",
+                        "service.version", "2.0.0",
+                        "host.name", "checkout-3")))
+                .attributes(new HashMap<>(java.util.Map.of("http.route", 
"/checkout")))
+                .build();
+        when(historyDataReader.queryLogsByMultipleConditions(any(), any(), 
any(),
+                any(), any(), any(), any())).thenReturn(List.of(stableLog, 
canaryLog, cartLog, otherVersionLog));
+
+        mockMvc.perform(MockMvcRequestBuilders.get("/api/logs/list")
+                        .param("resourceFilter", "service.version IN 
(\"1.2.3\", '1.2.4') "
+                                + "and host.name NOT IN ('checkout-canary')")
+                        .param("attributeFilter", "http.route IN 
('/checkout')"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value((int) 
CommonConstants.SUCCESS_CODE))
+                .andExpect(jsonPath("$.data.content.length()").value(1))
+                .andExpect(jsonPath("$.data.content[0].body").value("stable 
checkout log"))
+                .andExpect(jsonPath("$.data.totalElements").value(1));
+
+        verify(historyDataReader).queryLogsByMultipleConditions(any(), any(), 
any(),
+                any(), any(), any(), any());
+        verify(historyDataReader, 
never()).countLogsByMultipleConditions(any(), any(), any(),
+                any(), any(), any(), any());
+        verify(historyDataReader, 
never()).queryLogsByMultipleConditionsWithPagination(any(), any(),
+                any(), any(), any(), any(), any(), anyInt(), anyInt());
+    }
+
     @Test
     void testContextLogsReturnsSelectedLogWithBoundedBeforeAndAfterRows() 
throws Exception {
         long selectedTime = 1734005477630000000L;


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

Reply via email to