chore: import upstream snapshot with attribution
MCP Bash Server CI / Test MCP Bash Server (dev) (push) Has been cancelled
MCP Bash Server CI / Test MCP Bash Server (release) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:11:39 +08:00
commit c8cebdfeee
4654 changed files with 626756 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-e2e</artifactId>
<version>2.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>hertzbeat-log-e2e</artifactId>
<properties>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.resources.plugin.version>3.3.1</maven.resources.plugin.version>
<maven.dependency.plugin.version>3.6.1</maven.dependency.plugin.version>
</properties>
<dependencies>
<!-- HertzBeat modules -->
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-startup</artifactId>
<version>${hertzbeat.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-manager</artifactId>
<version>${hertzbeat.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-log</artifactId>
<version>${hertzbeat.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-alerter</artifactId>
<version>${hertzbeat.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-warehouse</artifactId>
<version>${hertzbeat.version}</version>
<scope>test</scope>
</dependency>
<!-- TestContainers -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test-autoconfigure</artifactId>
<scope>test</scope>
</dependency>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<!-- AssertJ -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<!-- Awaitility -->
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,242 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.log.alert;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.calculate.periodic.PeriodicAlertRuleScheduler;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.TestPropertySource;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.MountableFile;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.Mockito.doAnswer;
/**
* E2E tests for periodic log alert processing.
*/
@SpringBootTest(classes = org.apache.hertzbeat.startup.HertzBeatApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {
"warehouse.store.duckdb.enabled=false",
"warehouse.store.greptime.enabled=true"
})
@Slf4j
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class LogPeriodicAlertE2eTest {
private static final String VECTOR_IMAGE = "timberio/vector:latest-alpine";
private static final int VECTOR_PORT = 8686;
private static final String VECTOR_CONFIG_PATH = "/etc/vector/vector.yml";
private static final String ENV_HERTZBEAT_PORT = "HERTZBEAT_PORT";
private static final String GREPTIME_IMAGE = "greptime/greptimedb:latest";
private static final int GREPTIME_HTTP_PORT = 4000;
private static final int GREPTIME_GRPC_PORT = 4001;
private static final Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(120);
@LocalServerPort
private int port;
@Autowired
PeriodicAlertRuleScheduler periodicAlertRuleScheduler;
AlertDefine errorCountAlertByGroup;
AlertDefine errorCountAlertByIndividual;
@MockitoSpyBean
private AlarmCommonReduce alarmCommonReduce;
static GenericContainer<?> vector;
static GenericContainer<?> greptimedb;
static {
greptimedb = new GenericContainer<>(DockerImageName.parse(GREPTIME_IMAGE))
.withExposedPorts(GREPTIME_HTTP_PORT, GREPTIME_GRPC_PORT)
.withCommand("standalone", "start",
"--http-addr", "0.0.0.0:" + GREPTIME_HTTP_PORT,
"--rpc-bind-addr", "0.0.0.0:" + GREPTIME_GRPC_PORT)
.waitingFor(Wait.forListeningPorts(GREPTIME_HTTP_PORT, GREPTIME_GRPC_PORT))
.withStartupTimeout(CONTAINER_STARTUP_TIMEOUT);
greptimedb.start();
}
@DynamicPropertySource
static void greptimeProps(DynamicPropertyRegistry r) {
// Configure GreptimeDB storage endpoints (dynamic ports)
r.add("warehouse.store.greptime.http-endpoint", () -> "http://localhost:" + greptimedb.getMappedPort(GREPTIME_HTTP_PORT));
r.add("warehouse.store.greptime.grpc-endpoints", () -> "localhost:" + greptimedb.getMappedPort(GREPTIME_GRPC_PORT));
r.add("warehouse.store.greptime.username", () -> "");
r.add("warehouse.store.greptime.password", () -> "");
}
@BeforeAll
void setUpAll() throws InterruptedException {
// Setup test alert definitions
setupTestAlertDefines();
Testcontainers.exposeHostPorts(port);
// Wait for HertzBeat to be fully ready before starting Vector
log.info("Waiting for HertzBeat to be fully ready on port {}...", port);
Thread.sleep(5000); // Give HertzBeat time to fully initialize
vector = new GenericContainer<>(DockerImageName.parse(VECTOR_IMAGE))
.withExposedPorts(VECTOR_PORT)
.withCopyFileToContainer(MountableFile.forClasspathResource("vector.yml"), VECTOR_CONFIG_PATH)
.withCommand("--config", "/etc/vector/vector.yml", "--verbose")
.withLogConsumer(outputFrame -> log.info("Vector: {}", outputFrame.getUtf8String()))
.withNetwork(Network.newNetwork())
.withEnv(ENV_HERTZBEAT_PORT, String.valueOf(port))
.waitingFor(Wait.forListeningPort())
.withStartupTimeout(CONTAINER_STARTUP_TIMEOUT);
vector.start();
}
@Test
void testPeriodicLogAlertWithIndividualAlert() {
List<SingleAlert> capturedAlerts = new ArrayList<>();
doAnswer(invocation -> {
SingleAlert alert = invocation.getArgument(0);
capturedAlerts.add(alert);
return null;
}).when(alarmCommonReduce).reduceAndSendAlarm(any(SingleAlert.class));
periodicAlertRuleScheduler.updateSchedule(errorCountAlertByIndividual);
// Wait for periodic individual alert to be generated through AlarmCommonReduce
await().atMost(Duration.ofSeconds(60))
.pollInterval(Duration.ofSeconds(3))
.untilAsserted(() -> assertFalse(capturedAlerts.isEmpty(),
"Should have generated at least one periodic individual alert"));
// Verify alert properties
SingleAlert firstAlert = capturedAlerts.get(0);
assertNotNull(firstAlert, "First periodic alert should not be null");
assertEquals(CommonConstants.ALERT_STATUS_FIRING, firstAlert.getStatus(), "Alert should be in firing status");
assertNotNull(firstAlert.getLabels(), "Alert should have labels");
assertTrue(firstAlert.getLabels().containsKey(CommonConstants.LABEL_ALERT_SEVERITY), "Alert should have severity label");
assertEquals(CommonConstants.ALERT_SEVERITY_CRITICAL, firstAlert.getLabels().get(CommonConstants.LABEL_ALERT_SEVERITY), "Alert should have critical severity");
}
@Test
void testPeriodicLogAlertWithGroupAlert() {
List<List<SingleAlert>> capturedGroupAlerts = new ArrayList<>();
doAnswer(invocation -> {
@SuppressWarnings("unchecked")
List<SingleAlert> alerts = invocation.getArgument(1);
capturedGroupAlerts.add(alerts);
return null;
}).when(alarmCommonReduce).reduceAndSendAlarmGroup(anyMap(), anyList());
periodicAlertRuleScheduler.updateSchedule(errorCountAlertByGroup);
await().atMost(Duration.ofSeconds(60))
.pollInterval(Duration.ofSeconds(3))
.untilAsserted(() -> {
Optional<SingleAlert> matchedAlert = capturedGroupAlerts.stream()
.flatMap(List::stream)
.filter(alert -> alert.getLabels() != null)
.filter(alert -> String.valueOf(errorCountAlertByGroup.getId())
.equals(alert.getLabels().get(CommonConstants.LABEL_DEFINE_ID)))
.findFirst();
assertTrue(matchedAlert.isPresent(), "Should have captured group alert from target alert define");
SingleAlert anyAlert = matchedAlert.get();
assertEquals(CommonConstants.ALERT_STATUS_FIRING, anyAlert.getStatus(), "Alert should be in firing status");
assertEquals(CommonConstants.ALERT_SEVERITY_CRITICAL,
anyAlert.getLabels().get(CommonConstants.LABEL_ALERT_SEVERITY),
"Alert should have critical severity");
assertTrue(anyAlert.getTriggerTimes() >= 1, "Alert should indicate aggregated trigger times");
});
}
/**
* Setup test alert definitions for periodic log processing
*/
private void setupTestAlertDefines() {
// group
errorCountAlertByGroup = AlertDefine.builder()
.id(10L)
.name("periodic_error_count_alert_group")
.type(CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC)
.expr("SELECT COUNT(*) as error_count FROM hertzbeat_logs WHERE time_unix_nano > NOW() - INTERVAL '10 minute'")
.period(10) // Faster schedule for tests
.template("High error count detected: {{ error_count }} errors in last period")
.datasource("sql")
.enable(true)
.build();
Map<String, String> errorLabelsByGroup = new HashMap<>();
errorLabelsByGroup.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL);
errorLabelsByGroup.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP);
errorLabelsByGroup.put("type", "error_count");
errorCountAlertByGroup.setLabels(errorLabelsByGroup);
// individual
errorCountAlertByIndividual = AlertDefine.builder()
.id(11L)
.name("periodic_error_count_alert_individual")
.type(CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC)
.expr("SELECT COUNT(*) as error_count, severity_text FROM hertzbeat_logs "
+ "WHERE severity_text = 'ERROR' AND time_unix_nano > NOW() - INTERVAL '5 minute' GROUP BY severity_text HAVING COUNT(*) > 2")
.period(10) // Faster schedule for tests
.template("High error count detected: {{ error_count }} errors in last period")
.datasource("sql")
.enable(true)
.build();
Map<String, String> errorLabelsByIndividual = new HashMap<>();
errorLabelsByIndividual.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL);
errorLabelsByIndividual.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL);
errorLabelsByIndividual.put("type", "error_count");
errorCountAlertByIndividual.setLabels(errorLabelsByIndividual);
}
}
@@ -0,0 +1,207 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.log.alert;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.common.cache.CacheFactory;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.MountableFile;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.Mockito.doAnswer;
/**
* E2E tests for real-time log alert processing.
*/
@SpringBootTest(classes = org.apache.hertzbeat.startup.HertzBeatApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Slf4j
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class LogRealTimeAlertE2eTest {
private static final String VECTOR_IMAGE = "timberio/vector:latest-alpine";
private static final int VECTOR_PORT = 8686;
private static final String VECTOR_CONFIG_PATH = "/etc/vector/vector.yml";
private static final String ENV_HERTZBEAT_PORT = "HERTZBEAT_PORT";
private static final Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(180);
private static final Duration TEST_WAIT_TIMEOUT = Duration.ofSeconds(120);
@LocalServerPort
private int port;
private final List<SingleAlert> capturedAlerts = new ArrayList<>();
private final ArrayList<List<SingleAlert>> capturedGroupAlerts = new ArrayList<>();
@MockitoSpyBean
private AlarmCommonReduce alarmCommonReduce;
static GenericContainer<?> vector;
@BeforeAll
void setUpAll() throws InterruptedException {
// Setup test alert definitions
setupTestAlertDefines();
// Expose host ports for testcontainers
Testcontainers.exposeHostPorts(port);
// Wait for HertzBeat to be fully ready before starting Vector
log.info("Waiting for HertzBeat to be fully ready on port {}...", port);
Thread.sleep(5000); // Give HertzBeat time to fully initialize
vector = new GenericContainer<>(DockerImageName.parse(VECTOR_IMAGE))
.withExposedPorts(VECTOR_PORT)
.withCopyFileToContainer(MountableFile.forClasspathResource("vector.yml"), VECTOR_CONFIG_PATH)
.withCommand("--config", "/etc/vector/vector.yml", "--verbose")
.withLogConsumer(outputFrame -> log.info("Vector: {}", outputFrame.getUtf8String()))
.withNetwork(Network.newNetwork())
.withEnv(ENV_HERTZBEAT_PORT, String.valueOf(port))
.waitingFor(Wait.forListeningPort())
.withStartupTimeout(CONTAINER_STARTUP_TIMEOUT);
vector.start();
}
@Test
void testRealTimeLogAlertWithIndividualAlert() {
doAnswer(invocation -> {
SingleAlert alert = invocation.getArgument(0);
capturedAlerts.add(alert);
return null;
}).when(alarmCommonReduce).reduceAndSendAlarm(any(SingleAlert.class));
// Clear any existing captured alerts
capturedAlerts.clear();
// Wait for real alert to be generated through AlarmCommonReduce
await().atMost(TEST_WAIT_TIMEOUT)
.pollInterval(Duration.ofSeconds(3))
.untilAsserted(() -> assertFalse(capturedAlerts.isEmpty(),
"Should have generated at least one alert for error logs"));
// Verify alert properties
SingleAlert firstAlert = capturedAlerts.get(0);
assertNotNull(firstAlert, "First alert should not be null");
assertEquals(CommonConstants.ALERT_STATUS_FIRING, firstAlert.getStatus(), "Alert should be in firing status");
assertNotNull(firstAlert.getLabels(), "Alert should have labels");
assertTrue(firstAlert.getLabels().containsKey(CommonConstants.LABEL_ALERT_SEVERITY), "Alert should have severity label");
assertEquals(CommonConstants.ALERT_SEVERITY_CRITICAL, firstAlert.getLabels().get(CommonConstants.LABEL_ALERT_SEVERITY), "Alert should have critical severity");
}
@Test
void testRealTimeLogAlertWithGroupAlert() {
doAnswer(invocation -> {
@SuppressWarnings("unchecked")
List<SingleAlert> alerts = invocation.getArgument(1);
capturedGroupAlerts.add(alerts);
return null;
}).when(alarmCommonReduce).reduceAndSendAlarmGroup(anyMap(), anyList());
// Clear any existing captured alerts
capturedGroupAlerts.clear();
// Wait for group alert to be generated via AlarmCommonReduce
await().atMost(TEST_WAIT_TIMEOUT)
.pollInterval(Duration.ofSeconds(3))
.untilAsserted(() -> assertFalse(capturedGroupAlerts.isEmpty(),
"Should have generated high frequency warning group alert"));
List<SingleAlert> groupAlerts = capturedGroupAlerts.get(0);
assertNotNull(groupAlerts, "Group alerts should not be null");
assertTrue(groupAlerts.size() >= 2, "Group should contain at least 5 alerts");
SingleAlert anyAlert = groupAlerts.get(0);
assertEquals(CommonConstants.ALERT_STATUS_FIRING, anyAlert.getStatus(), "Alert should be in firing status");
assertNotNull(anyAlert.getLabels(), "Alert should have labels");
assertEquals(CommonConstants.ALERT_SEVERITY_WARNING, anyAlert.getLabels().get(CommonConstants.LABEL_ALERT_SEVERITY), "Alert should have warning severity");
assertTrue(anyAlert.getTriggerTimes() >= 2, "Alert should indicate high frequency trigger");
}
/**
* Setup test alert definitions for real-time log processing
*/
private void setupTestAlertDefines() {
List<AlertDefine> alertDefines = new ArrayList<>();
// Create error log alert definition
AlertDefine errorLogAlert = AlertDefine.builder()
.id(1L)
.name("error_log_alert")
.type(CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME)
.expr("log.severityText == 'ERROR'")
.period(10) // 10 seconds window
.times(1) // Trigger on first occurrence
.template("Error detected: {{ body }}")
.enable(true)
.build();
Map<String, String> errorLabels = new HashMap<>();
errorLabels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL);
errorLabels.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL);
errorLogAlert.setLabels(errorLabels);
alertDefines.add(errorLogAlert);
// Create high frequency warning alert definition
AlertDefine highFrequencyWarning = AlertDefine.builder()
.id(2L)
.name("high_frequency_warning")
.type(CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME)
.expr("log.severityText == 'ERROR'")
.period(10)
.times(2)
.template("High frequency warnings detected: {{ __rows__ }} warnings in 30 seconds")
.enable(true)
.build();
Map<String, String> warningLabels = new HashMap<>();
warningLabels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_WARNING);
warningLabels.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP);
highFrequencyWarning.setLabels(warningLabels);
alertDefines.add(highFrequencyWarning);
// Set alert definitions in cache
CacheFactory.setLogAlertDefineCache(alertDefines);
}
}
@@ -0,0 +1,114 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.log.ingestion;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.MountableFile;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/**
* E2E tests for log ingestion.
*/
@SpringBootTest(classes = org.apache.hertzbeat.startup.HertzBeatApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Slf4j
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class LogIngestionE2eTest {
private static final String VECTOR_IMAGE = "timberio/vector:latest-alpine";
private static final int VECTOR_PORT = 8686;
private static final String VECTOR_CONFIG_PATH = "/etc/vector/vector.yml";
private static final String ENV_HERTZBEAT_PORT = "HERTZBEAT_PORT";
private static final Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(120);
@LocalServerPort
private int port;
@Autowired
private CommonDataQueue commonDataQueue;
static GenericContainer<?> vector;
@BeforeAll
void setUpAll() throws InterruptedException {
Testcontainers.exposeHostPorts(port);
// Wait for HertzBeat to be fully ready before starting Vector
log.info("Waiting for HertzBeat to be fully ready on port {}...", port);
Thread.sleep(5000); // Give HertzBeat time to fully initialize
vector = new GenericContainer<>(DockerImageName.parse(VECTOR_IMAGE))
.withExposedPorts(VECTOR_PORT)
.withCopyFileToContainer(MountableFile.forClasspathResource("vector.yml"), VECTOR_CONFIG_PATH)
.withCommand("--config", "/etc/vector/vector.yml", "--verbose")
.withLogConsumer(outputFrame -> log.info("Vector: {}", outputFrame.getUtf8String()))
.withNetwork(Network.newNetwork())
.withEnv(ENV_HERTZBEAT_PORT, String.valueOf(port))
.waitingFor(Wait.forListeningPort())
.withStartupTimeout(CONTAINER_STARTUP_TIMEOUT);
vector.start();
}
@Test
void testLogIngestion() {
// Start polling for log entries from the queue
List<LogEntry> capturedLogs = new ArrayList<>();
// Wait for Vector to generate and send demo logs to HertzBeat
await().atMost(Duration.ofSeconds(60))
.pollInterval(Duration.ofSeconds(3))
.untilAsserted(() -> {
try {
LogEntry logEntry = commonDataQueue.pollLogEntry();
if (logEntry != null) {
capturedLogs.add(logEntry);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Test interrupted", e);
}
});
// Verify the captured logs
assertFalse(capturedLogs.isEmpty(), "No logs were captured from Vector");
LogEntry firstLog = capturedLogs.get(0);
assertNotNull(firstLog, "First log should not be null");
assertNotNull(firstLog.getBody(), "Log body should not be null");
assertNotNull(firstLog.getSeverityText(), "Severity text should not be null");
}
}
@@ -0,0 +1,187 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.log.storage;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeDbDataStorage;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.TestPropertySource;
import java.util.ArrayList;
import java.util.List;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.MountableFile;
import java.time.Duration;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/**
* E2E tests for GreptimeDB log storage.
*/
@SpringBootTest(classes = org.apache.hertzbeat.startup.HertzBeatApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {
"warehouse.store.duckdb.enabled=false",
"warehouse.store.greptime.enabled=true"
})
@Slf4j
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class GreptimeLogStorageE2eTest {
private static final String VECTOR_IMAGE = "timberio/vector:latest-alpine";
private static final int VECTOR_PORT = 8686;
private static final String VECTOR_CONFIG_PATH = "/etc/vector/vector.yml";
private static final String ENV_HERTZBEAT_PORT = "HERTZBEAT_PORT";
private static final String GREPTIME_IMAGE = "greptime/greptimedb:latest";
private static final int GREPTIME_HTTP_PORT = 4000;
private static final int GREPTIME_GRPC_PORT = 4001;
private static final Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(120);
@LocalServerPort
private int port;
@Autowired
private CommonDataQueue commonDataQueue;
@Autowired
private GreptimeDbDataStorage greptimeDbDataStorage;
static GenericContainer<?> vector;
static GenericContainer<?> greptimedb;
static {
greptimedb = new GenericContainer<>(DockerImageName.parse(GREPTIME_IMAGE))
.withExposedPorts(GREPTIME_HTTP_PORT, GREPTIME_GRPC_PORT)
.withCommand("standalone", "start",
"--http-addr", "0.0.0.0:" + GREPTIME_HTTP_PORT,
"--rpc-bind-addr", "0.0.0.0:" + GREPTIME_GRPC_PORT)
.waitingFor(Wait.forListeningPorts(GREPTIME_HTTP_PORT, GREPTIME_GRPC_PORT))
.withStartupTimeout(CONTAINER_STARTUP_TIMEOUT);
greptimedb.start();
}
@DynamicPropertySource
static void greptimeProps(DynamicPropertyRegistry r) {
r.add("warehouse.store.greptime.http-endpoint", () -> "http://localhost:" + greptimedb.getMappedPort(GREPTIME_HTTP_PORT));
r.add("warehouse.store.greptime.grpc-endpoints", () -> "localhost:" + greptimedb.getMappedPort(GREPTIME_GRPC_PORT));
r.add("warehouse.store.greptime.username", () -> "");
r.add("warehouse.store.greptime.password", () -> "");
}
@BeforeAll
void setUpAll() throws InterruptedException {
// Expose host ports for testcontainers
Testcontainers.exposeHostPorts(port);
// Wait for HertzBeat to be fully ready before starting Vector
log.info("Waiting for HertzBeat to be fully ready on port {}...", port);
Thread.sleep(5000); // Give HertzBeat time to fully initialize
vector = new GenericContainer<>(DockerImageName.parse(VECTOR_IMAGE))
.withExposedPorts(VECTOR_PORT)
.withCopyFileToContainer(MountableFile.forClasspathResource("vector.yml"), VECTOR_CONFIG_PATH)
.withCommand("--config", "/etc/vector/vector.yml", "--verbose")
.withLogConsumer(outputFrame -> log.info("Vector: {}", outputFrame.getUtf8String()))
.withNetwork(Network.newNetwork())
.withEnv(ENV_HERTZBEAT_PORT, String.valueOf(port))
.waitingFor(Wait.forListeningPort())
.withStartupTimeout(CONTAINER_STARTUP_TIMEOUT);
vector.start();
}
@Test
void testLogStorageToGreptimeDb() {
log.info("GreptimeDbDataStorage serverAvailable: {}", greptimeDbDataStorage.isServerAvailable());
List<LogEntry> capturedLogs = new ArrayList<>();
// Wait for Vector to generate and send logs to HertzBeat
await().atMost(Duration.ofSeconds(30))
.pollInterval(Duration.ofSeconds(3))
.untilAsserted(() -> {
// Poll log entries from the queue (non-blocking)
try {
LogEntry logEntry = commonDataQueue.pollLogEntry();
if (logEntry != null) {
capturedLogs.add(logEntry);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Test interrupted", e);
}
// Assert that we have captured at least some logs
assertFalse(capturedLogs.isEmpty(), "Should have captured at least one log entry");
});
// Verify the captured logs
assertFalse(capturedLogs.isEmpty(), "No logs were captured from Vector");
LogEntry firstLog = capturedLogs.get(0);
assertNotNull(firstLog, "First log should not be null");
assertNotNull(firstLog.getBody(), "Log body should not be null");
assertNotNull(firstLog.getSeverityText(), "Severity text should not be null");
// Directly write logs to GreptimeDB to test storage functionality
log.info("Directly writing {} captured logs to GreptimeDB", capturedLogs.size());
greptimeDbDataStorage.saveLogDataBatch(capturedLogs);
// Give some time for the write to complete
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// Additional wait to ensure logs are persisted to GreptimeDB
await().atMost(Duration.ofSeconds(30))
.pollInterval(Duration.ofSeconds(2))
.untilAsserted(() -> {
// Query GreptimeDB directly to verify data persistence
List<LogEntry> storedLogs = queryStoredLogs();
log.info("Queried {} logs from GreptimeDB", storedLogs.size());
assertFalse(storedLogs.isEmpty(), "Should have logs stored in GreptimeDB");
});
}
/**
* Helper method to query stored logs directly from GreptimeDB
*/
private List<LogEntry> queryStoredLogs() {
// Query without time condition to verify data exists
List<LogEntry> result = greptimeDbDataStorage.queryLogsByMultipleConditions(
null, null, null, null, null, null, null);
log.info("queryLogsByMultipleConditions returned {} entries", result.size());
return result;
}
}
@@ -0,0 +1,136 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
## -- sureness.yml account source -- ##
# config the resource restful api that need auth protection, base rbac
# rule: api===method===role
# eg: /api/v1/source1===get===[admin] means /api/v2/host===post support role[admin] access.
# eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by any role.
resourceRole:
- /api/account/auth/refresh===post===[admin,user,guest]
- /api/apps/**===get===[admin,user,guest]
- /api/monitor/**===get===[admin,user,guest]
- /api/monitor/**===post===[admin,user]
- /api/monitor/**===put===[admin,user]
- /api/monitor/**===delete==[admin]
- /api/monitors/**===get===[admin,user,guest]
- /api/monitors/**===post===[admin,user]
- /api/monitors/**===put===[admin,user]
- /api/monitors/**===delete===[admin]
- /api/alert/**===get===[admin,user,guest]
- /api/alert/**===post===[admin,user]
- /api/alert/**===put===[admin,user]
- /api/alert/**===delete===[admin]
- /api/alerts/**===get===[admin,user,guest]
- /api/alerts/**===post===[admin,user]
- /api/alerts/**===put===[admin,user]
- /api/alerts/**===delete===[admin]
- /api/notice/**===get===[admin,user,guest]
- /api/notice/**===post===[admin,user]
- /api/notice/**===put===[admin,user]
- /api/notice/**===delete===[admin]
- /api/tag/**===get===[admin,user,guest]
- /api/tag/**===post===[admin,user]
- /api/tag/**===put===[admin,user]
- /api/tag/**===delete===[admin]
- /api/summary/**===get===[admin,user,guest]
- /api/summary/**===post===[admin,user]
- /api/summary/**===put===[admin,user]
- /api/summary/**===delete===[admin]
- /api/collector/**===get===[admin,user,guest]
- /api/collector/**===post===[admin,user]
- /api/collector/**===put===[admin,user]
- /api/collector/**===delete===[admin]
- /api/status/page/**===get===[admin,user,guest]
- /api/status/page/**===post===[admin,user]
- /api/status/page/**===put===[admin,user]
- /api/status/page/**===delete===[admin]
- /api/grafana/**===get===[admin,user,guest]
- /api/grafana/**===post===[admin,user]
- /api/grafana/**===put===[admin,user]
- /api/grafana/**===delete===[admin]
- /api/bulletin/**===get===[admin,user,guest]
- /api/bulletin/**===post===[admin,user]
- /api/bulletin/**===put===[admin,user]
- /api/bulletin/**===delete===[admin]
- /api/sse/**===get===[admin,user]
- /api/sse/**===post===[admin,user]
- /api/chat/**===get===[admin,user]
- /api/chat/**===post===[admin,user]
- /api/logs/ingest/**===post===[admin,user]
# config the resource restful api that need bypass auth protection
# rule: api===method
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
excludedResource:
- /api/alert/sse/**===*
- /api/logs/sse/**===*
- /api/account/auth/**===*
- /api/i18n/**===get
- /api/apps/hierarchy===get
- /api/push/**===*
- /api/status/page/public/**===*
- /api/manager/sse/**===*
# web ui resource
- /===get
- /assets/**===get
- /dashboard/**===get
- /monitors/**===get
- /alert/**===get
- /account/**===get
- /setting/**===get
- /passport/**===get
- /status/**===get
- /**/*.html===get
- /**/*.js===get
- /**/*.css===get
- /**/*.ico===get
- /**/*.ttf===get
- /**/*.png===get
- /**/*.gif===get
- /**/*.jpg===get
- /**/*.svg===get
- /**/*.json===get
- /**/*.woff===get
- /**/*.eot===get
# swagger ui resource
- /swagger-resources/**===get
- /v2/api-docs===get
- /v3/api-docs===get
# h2 database
- /h2-console/**===*
# account info config
# eg: admin has role [admin,user], password is hertzbeat
# eg: tom has role [user], password is hertzbeat
# eg: lili has role [guest], plain password is lili, salt is 123, salted password is 1A676730B0C7F54654B0E09184448289
account:
- appId: admin
credential: hertzbeat
role: [admin]
- appId: tom
credential: hertzbeat
role: [user]
- appId: guest
credential: hertzbeat
role: [guest]
- appId: lili
# credential = MD5(password + salt)
# plain password: hertzbeat
# attention: digest authentication does not support salted encrypted password accounts
credential: 94C6B34E7A199A9F9D4E1F208093B489
salt: 123
role: [user]
@@ -0,0 +1,126 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Vector configuration for HertzBeat E2E testing
api:
enabled: true
address: "0.0.0.0:8686"
sources:
generate_syslog:
type: "demo_logs"
format: "syslog"
count: 100000
interval: 1
transforms:
remap_syslog:
inputs: ["generate_syslog"]
type: "remap"
source: |
syslog = parse_syslog!(.message)
severity_text = if includes(["emerg", "err", "crit", "alert"], syslog.severity) {
"ERROR"
} else if syslog.severity == "warning" {
"WARN"
} else if syslog.severity == "debug" {
"DEBUG"
} else if includes(["info", "notice"], syslog.severity) {
"INFO"
} else {
syslog.severity
}
severity_number = if includes(["emerg", "err", "crit", "alert"], syslog.severity) {
17
} else if syslog.severity == "warning" {
13
} else if syslog.severity == "debug" {
5
} else if includes(["info", "notice"], syslog.severity) {
9
} else {
9
}
.resourceLogs = [{
"resource": {
"attributes": [
{ "key": "source_type", "value": { "stringValue": .source_type } },
{ "key": "service.name", "value": { "stringValue": syslog.appname } },
{ "key": "host.hostname", "value": { "stringValue": syslog.hostname } }
]
},
"scopeLogs": [{
"scope": {
"name": syslog.msgid
},
"logRecords": [{
"timeUnixNano": to_unix_timestamp!(syslog.timestamp, unit: "nanoseconds"),
"body": { "stringValue": syslog.message },
"severityText": severity_text,
"severityNumber": severity_number,
"attributes": [
{ "key": "syslog.procid", "value": { "stringValue": to_string(syslog.procid) } },
{ "key": "syslog.facility", "value": { "stringValue": syslog.facility } },
{ "key": "syslog.version", "value": { "stringValue": to_string(syslog.version) } }
]
}]
}]
}]
del(.message)
del(.timestamp)
del(.service)
del(.source_type)
sinks:
# Debug sink to log all processed data
debug_console:
inputs: [ "remap_syslog" ]
type: console
encoding:
codec: json
# Send to HertzBeat with increased timeout and retry
emit_syslog:
inputs: [ "remap_syslog" ]
type: opentelemetry
protocol:
type: http
uri: "http://host.testcontainers.internal:${HERTZBEAT_PORT:-1157}/api/logs/ingest/otlp"
method: post
encoding:
codec: json
framing:
method: newline_delimited
auth:
strategy: basic
user: admin
password: hertzbeat
# Increase timeout and retry settings for stability in CI environments
request:
headers:
content-type: application/json
timeout_secs: 60
retry_attempts: 10
retry_initial_backoff_secs: 2
retry_max_duration_secs: 120
# Batch settings for better throughput
batch:
max_bytes: 524288
timeout_secs: 1