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]