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]