feat(syslog): 完成高吞吐可靠链路与统计存储优化

- 新增接入 WAL、检查点、背压恢复及可靠分发机制,完善 Kafka 幂等发布、分区、重试、DLT 和补偿流程
- 引入规则预编译、批内快照及 JetCache/Caffeine 两级缓存,支持缓存失效广播、影子对账和执行指标
- 升级 InfluxDB V2 原始日志模型,统一接收时间与 Point 时间,支持按日 measurement、UUID 分桶及 V1/V2 双读
- 重构达梦标准化批量写入与统计链路,支持事务内直写、增量投影、幂等控制、窗口水位和日期切换
- 完善告警处理、配置校验、运行监控及测试覆盖
This commit is contained in:
王绍宏
2026-08-20 00:18:33 +08:00
parent a3852fc0f9
commit 29445c6825
303 changed files with 20419 additions and 4481 deletions
@@ -0,0 +1,60 @@
package com.Modules.NormalData;
import com.common.mapper.DmColumnMapper;
import com.common.mapper.DmNormalizeRuleMapper;
import com.common.service.DeviceCollectTaskService;
import com.common.service.DeviceDeviceService;
import com.common.service.DmColumnService;
import com.common.service.DmNormalizeRuleService;
import com.common.service.LogDataCompleteService;
import com.common.service.LogDataFilterService;
import com.common.service.SyslogNonNormalMessageService;
import com.common.service.SyslogNormalDataService;
import com.config.AppProperties;
import com.config.KafkaConsumerProperties;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
class LogNormalProcessorMissingRuleTest {
@Test
void missingDeviceRuleKeepsRawAndReturnsFilteredWithoutRetryFailure() {
DmNormalizeRuleService ruleService = mock(DmNormalizeRuleService.class);
DmColumnService columnService = mock(DmColumnService.class);
SyslogNormalDataService normalDataService = mock(SyslogNormalDataService.class);
SyslogNonNormalMessageService nonNormalMessageService = mock(SyslogNonNormalMessageService.class);
LogDataFilterService filterService = mock(LogDataFilterService.class);
LogDataCompleteService completeService = mock(LogDataCompleteService.class);
DeviceCollectTaskService collectTaskService = mock(DeviceCollectTaskService.class);
DeviceDeviceService deviceService = mock(DeviceDeviceService.class);
DmColumnMapper columnMapper = mock(DmColumnMapper.class);
DmNormalizeRuleMapper ruleMapper = mock(DmNormalizeRuleMapper.class);
KafkaConsumerProperties kafkaProperties = mock(KafkaConsumerProperties.class);
AppProperties appProperties = mock(AppProperties.class);
when(ruleService.selectByDeviceIdAuto(127L)).thenReturn(Collections.emptyList());
LogNormalProcessor processor = new LogNormalProcessor(
ruleService, columnService, normalDataService, nonNormalMessageService,
filterService, completeService, collectTaskService, deviceService,
columnMapper, ruleMapper, kafkaProperties, appProperties);
String message = "[receive_time=20260811185857099 event_id=20260811-test "
+ "device_id=127 device_name=安恒防火墙 vendor=安恒 data_type=json device_collect_id=1]"
+ "<5>Aug 11 18:58:57 DAS-Gateway;ips: event_id=999751;action=pass";
assertEquals(LogNormalProcessor.ProcessingOutcome.FILTERED,
processor.process(message, "20260811-test", "agent-01-syslog-topic"));
assertEquals(LogNormalProcessor.ProcessingOutcome.FILTERED,
processor.process(message, "20260811-test-2", "agent-01-syslog-topic"));
verify(ruleService, times(1)).selectByDeviceIdAuto(127L);
verifyNoInteractions(normalDataService, nonNormalMessageService, filterService, completeService);
}
}
@@ -0,0 +1,23 @@
package com.Modules.NormalData;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
class LogNormalProcessorReceiveTimeTest {
@Test
void parsesIngressReceiveTimeForNonNormalRecord() {
assertEquals(LocalDateTime.of(2026, 8, 11, 7, 36, 21, 154_000_000),
LogNormalProcessor.parseReceiveTime("20260811073621154"));
}
@Test
void missingOrInvalidReceiveTimeStaysNull() {
assertNull(LogNormalProcessor.parseReceiveTime(null));
assertNull(LogNormalProcessor.parseReceiveTime("invalid"));
}
}
@@ -0,0 +1,28 @@
package com.Modules.NormalData;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import static org.junit.jupiter.api.Assertions.assertEquals;
class SysLogProcessorTimeTest {
@Test
void receiveTimeDeterminesBothPointTimestampAndMeasurementDate() {
SysLogProcessor.ResolvedInfluxTime resolved = SysLogProcessor.resolveInfluxTime(
"20260811035557109", Instant.parse("2026-08-09T00:00:00Z"));
assertEquals(Instant.parse("2026-08-10T19:55:57.109Z").toEpochMilli(),
resolved.getTimestampMs());
assertEquals("20260811", resolved.getMeasurementDate());
}
@Test
void invalidReceiveTimeIsRejectedInsteadOfUsingConsumerTime() {
Instant firstReceivedAt = Instant.parse("2026-08-10T19:55:57.109Z");
org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class,
() -> SysLogProcessor.resolveInfluxTime("invalid", firstReceivedAt));
}
}
@@ -0,0 +1,149 @@
package com.Modules.NormalData;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.config.CacheConfig;
import com.influxdb.exceptions.InfluxException;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.concurrent.SettableListenableFuture;
import java.net.ConnectException;
import java.net.SocketException;
import java.time.Instant;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
class SyslogFailureRouterTest {
private KafkaTemplate<String, String> template;
private SyslogFailureRouter router;
private ConsumerRecord<String, String> record;
@BeforeEach
void setUp() {
template = mock(KafkaTemplate.class);
router = new SyslogFailureRouter(template, new ObjectMapper().findAndRegisterModules());
ReflectionTestUtils.setField(router, "retry1mTopic", "raw.RETRY.1M");
ReflectionTestUtils.setField(router, "retry10mTopic", "raw.RETRY.10M");
ReflectionTestUtils.setField(router, "retry1hTopic", "raw.RETRY.1H");
ReflectionTestUtils.setField(router, "dltTopic", "raw.DLT");
ReflectionTestUtils.setField(router, "publishTimeoutMs", 1000L);
ReflectionTestUtils.setField(router, "consumerVersion", "test");
record = new ConsumerRecord<>("raw", 2, 42L, "device-1", "ciphertext");
}
@Test
void retryIsTerminalOnlyAfterBrokerAcknowledgment() {
SettableListenableFuture<SendResult<String, String>> acknowledged = new SettableListenableFuture<>();
acknowledged.set(null);
when(template.send(eq("raw.RETRY.1M"), anyString(), anyString())).thenReturn(acknowledged);
SyslogFailureRouter.MessageContext context = router.resolve(record);
SyslogFailureRouter.TerminalState state = router.route(record, context, new SocketException("reset"));
assertEquals(SyslogFailureRouter.TerminalState.RETRY_PUBLISHED, state);
verify(template).send(eq("raw.RETRY.1M"), eq(context.getEventId()), contains("SYSLOG_RELIABILITY_V1"));
}
@Test
void retryEnvelopePreservesOriginalTopicAndFallbackPointTime() throws Exception {
SettableListenableFuture<SendResult<String, String>> acknowledged = new SettableListenableFuture<>();
acknowledged.set(null);
when(template.send(eq("raw.RETRY.1M"), anyString(), anyString())).thenReturn(acknowledged);
SyslogFailureRouter.MessageContext context = router.resolve(record);
context.setPointTimeNs(1_786_636_800_000_000_123L);
org.mockito.ArgumentCaptor<String> body = org.mockito.ArgumentCaptor.forClass(String.class);
router.route(record, context, new SocketException("reset"));
verify(template).send(eq("raw.RETRY.1M"), anyString(), body.capture());
SyslogFailureRouter.FailureEnvelope envelope = new ObjectMapper().findAndRegisterModules()
.readValue(body.getValue(), SyslogFailureRouter.FailureEnvelope.class);
assertEquals("raw", envelope.getOriginalTopic());
assertEquals(context.getPointTimeNs(), envelope.getPointTimeNs());
assertFalse(body.getValue().contains("schemaVersion"));
assertFalse(body.getValue().contains("schema_version"));
}
@Test
void publishFailureNeverBecomesTerminal() {
SettableListenableFuture<SendResult<String, String>> failed = new SettableListenableFuture<>();
failed.setException(new RuntimeException("broker unavailable"));
when(template.send(anyString(), anyString(), anyString())).thenReturn(failed);
assertThrows(SyslogFailureRouter.FailurePublishException.class,
() -> router.route(record, router.resolve(record), new SocketException("reset")));
}
@Test
void systemicFailurePausesWithoutDltStorm() {
SyslogFailureRouter.TerminalState state = router.route(
record, router.resolve(record), new ConnectException("database unavailable"));
assertEquals(SyslogFailureRouter.TerminalState.PAUSED, state);
verifyNoInteractions(template);
}
@Test
void influxDependencyFailurePausesWithoutDltStorm() {
SyslogFailureRouter.TerminalState state = router.route(
record, router.resolve(record), new InfluxException("influx unavailable"));
assertEquals(SyslogFailureRouter.TerminalState.PAUSED, state);
verifyNoInteractions(template);
}
@Test
void configurationFailureHasIndependentCategory() {
assertEquals(SyslogFailureRouter.FailureCategory.CONFIGURATION_RULE,
router.classify(new IllegalStateException("标准化规则配置错误")));
}
@Test
void permanentDataErrorGoesDirectlyToDlt() {
SettableListenableFuture<SendResult<String, String>> acknowledged = new SettableListenableFuture<>();
acknowledged.set(null);
when(template.send(eq("raw.DLT"), anyString(), anyString())).thenReturn(acknowledged);
SyslogFailureRouter.TerminalState state = router.route(
record, router.resolve(record), new IllegalArgumentException("bad payload"));
assertEquals(SyslogFailureRouter.TerminalState.DLT_PUBLISHED, state);
}
@Test
void fallbackEventIdUsesShanghaiReceiveDateAndIsDeterministic() {
Instant firstReceivedAt = Instant.parse("2026-08-10T19:55:57.109Z");
String first = SyslogFailureRouter.fallbackEventId(record, firstReceivedAt);
String second = SyslogFailureRouter.fallbackEventId(record, firstReceivedAt);
assertTrue(first.matches("20260811-[0-9a-f-]{36}"));
assertEquals(first, second);
}
@Test
void legacyTypedRetryEnvelopeKeepsOriginalPayloadAndEventId() {
router = new SyslogFailureRouter(template, new CacheConfig().objectMapper());
String legacyEnvelope = "{\"@class\":\"com.Modules.NormalData.SyslogFailureRouter$FailureEnvelope\","
+ "\"envelopeType\":\"SYSLOG_RELIABILITY_V1\","
+ "\"eventId\":\"20260811-existing-event\","
+ "\"firstReceivedAt\":\"2026-08-11T10:08:58.696Z\","
+ "\"attemptCount\":1,\"originalPayload\":\"ciphertext\"}";
ConsumerRecord<String, String> retryRecord = new ConsumerRecord<>(
"raw.RETRY.1M", 1, 576L, "event", legacyEnvelope);
SyslogFailureRouter.MessageContext context = router.resolve(retryRecord);
assertEquals("20260811-existing-event", context.getEventId());
assertEquals("ciphertext", context.getPayload());
assertEquals(1, context.getAttemptCount());
assertEquals(Instant.parse("2026-08-11T10:08:58.696Z"), context.getFirstReceivedAt());
}
}
@@ -0,0 +1,28 @@
package com.Modules.NormalData;
import org.junit.jupiter.api.Test;
import org.springframework.kafka.annotation.KafkaListener;
import java.lang.reflect.Method;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/** 防止平台端监听器的 ID、消费组和启停开关在后续合并中被误改。 */
class SyslogKafkaTopologyTest {
@Test
void rawListenerUsesPlatformIdentityAndExplicitGroup() throws Exception {
Method method = SysLogProcessor.class.getMethod(
"consumeAndParallelProcess", List.class,
org.springframework.kafka.support.Acknowledgment.class);
KafkaListener listener = method.getAnnotation(KafkaListener.class);
assertNotNull(listener);
assertEquals("syslog-consumer-raw", listener.id());
assertEquals("${spring.kafka.consumer.group-id}", listener.groupId());
assertEquals("${syslog.raw-consumer.enabled:true}", listener.autoStartup());
assertEquals("${spring.kafka.listener.concurrency}", listener.concurrency());
}
}
@@ -0,0 +1,19 @@
package com.common.mapper;
import org.apache.ibatis.annotations.Select;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.assertNull;
class SecExceptionAlgorithmMapperDefinitionTest {
@Test
void findByIdSqlComesOnlyFromXml() throws Exception {
Method findById = SecExceptionAlgorithmMapper.class.getMethod("findById", Long.class);
assertNull(findById.getAnnotation(Select.class),
"findById must not be declared by both @Select and mapper XML");
}
}
@@ -0,0 +1,84 @@
package com.common.schedule;
import com.common.entity.AnalysisAnalysisRule;
import com.common.entity.AnalysisGroupBy;
import com.common.entity.AnalysisGroupByWindow;
import com.common.mapper.AnalysisAnalysisRuleMapper;
import com.common.mapper.AnalysisGroupByMapper;
import com.common.mapper.AnalysisGroupByWindowMapper;
import com.common.service.AnalysisCheckpointService;
import com.common.service.AnalysisRuleService;
import com.common.service.RuleExecutionTimeService;
import com.common.service.RuleWindowCalculator;
import com.common.service.RuleWindowContext;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class RealtimeAnalysisSchedulerWindowTest {
@Test
void schedulerUsesOneWindowForContextCheckpointAndNextBoundary() {
RealtimeAnalysisScheduler scheduler = new RealtimeAnalysisScheduler();
AnalysisRuleService ruleService = mock(AnalysisRuleService.class);
RuleExecutionTimeService executionTimeService = mock(RuleExecutionTimeService.class);
AnalysisCheckpointService checkpointService = mock(AnalysisCheckpointService.class);
AnalysisAnalysisRuleMapper ruleMapper = mock(AnalysisAnalysisRuleMapper.class);
AnalysisGroupByMapper groupByMapper = mock(AnalysisGroupByMapper.class);
AnalysisGroupByWindowMapper windowMapper = mock(AnalysisGroupByWindowMapper.class);
RuleWindowContext context = new RuleWindowContext();
RuleWindowCalculator calculator = new RuleWindowCalculator();
ReflectionTestUtils.setField(calculator, "allowedLatenessSeconds", 0L);
ReflectionTestUtils.setField(scheduler, "analysisRuleService", ruleService);
ReflectionTestUtils.setField(scheduler, "ruleExecutionTimeService", executionTimeService);
ReflectionTestUtils.setField(scheduler, "checkpointService", checkpointService);
ReflectionTestUtils.setField(scheduler, "ruleWindowContext", context);
ReflectionTestUtils.setField(scheduler, "ruleWindowCalculator", calculator);
ReflectionTestUtils.setField(scheduler, "ruleMapper", ruleMapper);
ReflectionTestUtils.setField(scheduler, "groupByMapper", groupByMapper);
ReflectionTestUtils.setField(scheduler, "groupByWindowMapper", windowMapper);
ReflectionTestUtils.setField(scheduler, "realtimeEnabled", true);
AnalysisAnalysisRule rule = AnalysisAnalysisRule.builder()
.ruleId("rule-1").ruleName("rule").version(3).build();
AnalysisGroupBy groupBy = AnalysisGroupBy.builder().id(1L).build();
AnalysisGroupByWindow config = AnalysisGroupByWindow.builder()
.windowType("TUMBLE").tumbleWindowSize(5).tumbleWindowSizeUnit("m").build();
LocalDateTime end = LocalDateTime.now().minusMinutes(1).withSecond(0).withNano(0);
LocalDateTime start = end.minusMinutes(5);
when(ruleMapper.selectActiveRulesByRunMode("realtime"))
.thenReturn(Collections.singletonList(rule));
when(groupByMapper.selectByRuleId("rule-1")).thenReturn(Collections.singletonList(groupBy));
when(windowMapper.selectByGroupById(1)).thenReturn(config);
when(checkpointService.earliestIncompleteRuleEnd("rule-1", "3:window-v2"))
.thenReturn(Optional.empty());
when(executionTimeService.getNextExecuteTime("rule-1")).thenReturn(end);
when(checkpointService.claimRuleWindow("rule-1", "3:window-v2", start, end)).thenReturn(true);
when(ruleService.executeRealtimeRule("rule-1")).thenAnswer(invocation -> {
assertTrue(context.current().isPresent());
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("results", Collections.emptyList());
return result;
});
scheduler.checkAndExecuteRules();
verify(checkpointService).completeRuleWindow("rule-1", "3:window-v2",
start, end, 0L, 0L);
verify(executionTimeService).updateNextExecuteTime(rule, config, end);
assertTrue(!context.current().isPresent());
}
}
@@ -0,0 +1,38 @@
package com.common.schedule;
import com.common.service.AccessLogAlertService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class SchedulerSwitchConfigurationTest {
@Test
void healthCheckEnabledPropertyActuallyControlsSchedulerBean() {
ConditionalOnProperty condition = AlarmHealthCheckScheduler.class
.getAnnotation(ConditionalOnProperty.class);
assertNotNull(condition);
assertEquals("alarm.health-check", condition.prefix());
assertEquals("enabled", condition.name()[0]);
assertEquals("true", condition.havingValue());
assertTrue(condition.matchIfMissing());
}
@Test
void bothCronExpressionsCanBeOverriddenWithoutCodeChange() throws Exception {
Method healthTask = AlarmHealthCheckScheduler.class.getMethod("scheduledHealthCheck");
Method algorithmTask = AccessLogAlertService.class.getMethod("safeProcessTask");
assertEquals("${alarm.health-check.cron:0 */30 * * * ?}",
healthTask.getAnnotation(Scheduled.class).cron());
assertEquals("${algorithm.processing.cron:0 */1 * * * ?}",
algorithmTask.getAnnotation(Scheduled.class).cron());
}
}
@@ -0,0 +1,112 @@
package com.common.service;
import com.common.entity.AlarmVisit;
import com.common.mapper.AlarmVisitMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.SimpleTransactionStatus;
import java.sql.SQLException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class AlarmVisitIdempotentWriterTest {
private AlarmVisitMapper mapper;
private PlatformTransactionManager transactionManager;
private AlarmVisitIdempotentWriter writer;
@BeforeEach
void setUp() {
mapper = mock(AlarmVisitMapper.class);
transactionManager = mock(PlatformTransactionManager.class);
when(transactionManager.getTransaction(any(TransactionDefinition.class)))
.thenReturn(new SimpleTransactionStatus());
writer = new AlarmVisitIdempotentWriter(mapper, transactionManager);
}
@Test
void firstInsertCommitsAndReturnsInserted() {
AlarmVisitIdempotentWriter.WriteResult result = writer.writeOne(alarm("event-1", "scan"));
assertEquals(AlarmVisitIdempotentWriter.WriteResult.INSERTED, result);
verify(mapper).insert(any(AlarmVisit.class));
verify(transactionManager).commit(any());
verify(mapper, never()).countByAlgorithmEvent(anyString(), anyString(), anyString(), anyString());
}
@Test
void dmGenericIntegrityExceptionIsIdempotentOnlyWhenBusinessKeyExists() {
AlarmVisit alarm = alarm("event-1", "scan");
doThrow(dmUniqueViolation()).when(mapper).insert(alarm);
when(mapper.countByAlgorithmEvent("16", "v1", "event-1", "scan")).thenReturn(1);
AlarmVisitIdempotentWriter.WriteResult result = writer.writeOne(alarm);
assertEquals(AlarmVisitIdempotentWriter.WriteResult.ALREADY_EXISTS, result);
verify(transactionManager).rollback(any());
verify(mapper).countByAlgorithmEvent("16", "v1", "event-1", "scan");
}
@Test
void uniqueViolationWithoutMatchingBusinessRowIsNotSwallowed() {
AlarmVisit alarm = alarm("event-1", "scan");
DataIntegrityViolationException failure = dmUniqueViolation();
doThrow(failure).when(mapper).insert(alarm);
when(mapper.countByAlgorithmEvent("16", "v1", "event-1", "scan")).thenReturn(0);
DataIntegrityViolationException thrown = assertThrows(
DataIntegrityViolationException.class, () -> writer.writeOne(alarm));
assertEquals(failure, thrown);
}
@Test
void nonUniqueIntegrityViolationIsNotSwallowedOrQueriedAsDuplicate() {
AlarmVisit alarm = alarm("event-1", "scan");
DataIntegrityViolationException failure = new DataIntegrityViolationException(
"value too long", new SQLException("data too long", "22001"));
doThrow(failure).when(mapper).insert(alarm);
assertThrows(DataIntegrityViolationException.class, () -> writer.writeOne(alarm));
verify(mapper, never()).countByAlgorithmEvent(anyString(), anyString(), anyString(), anyString());
}
@Test
void missingIdempotencyColumnFailsBeforeDatabaseAccess() {
AlarmVisit alarm = alarm("event-1", null);
assertThrows(IllegalArgumentException.class, () -> writer.writeOne(alarm));
verify(mapper, never()).insert(any(AlarmVisit.class));
}
private AlarmVisit alarm(String eventId, String resultType) {
return AlarmVisit.builder()
.id("stable-id")
.algorithmId("16")
.algorithmVersion("v1")
.eventId(eventId)
.resultType(resultType)
.build();
}
private DataIntegrityViolationException dmUniqueViolation() {
return new DataIntegrityViolationException(
"达梦写入失败",
new SQLException("Violate unique constraint on [alarm_visit_p_20260811][INDEX33559268_33559185]",
"23000"));
}
}
@@ -0,0 +1,55 @@
package com.common.service;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.annotation.Transactional;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
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.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.when;
class AnalysisCheckpointServiceTest {
@Test
void ruleLeaseUsesFractionalDayInsteadOfIntegerDivision() {
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
when(jdbcTemplate.update(anyString(), any(Object[].class))).thenReturn(1);
AnalysisCheckpointService service = new AnalysisCheckpointService(jdbcTemplate);
ReflectionTestUtils.setField(service, "leaseSeconds", 600);
service.claimRuleWindow("rule-1", "1:window-v2",
LocalDateTime.of(2026, 8, 10, 17, 35),
LocalDateTime.of(2026, 8, 10, 17, 42));
assertTrue(mockingDetails(jdbcTemplate).getInvocations().stream()
.map(invocation -> String.valueOf(invocation.getRawArguments()[0]))
.anyMatch(sql -> sql.contains("lease_until=SYSDATE+(?/86400.0)")));
}
@Test
void terminalCheckpointUpdatesAreTransactional() throws Exception {
assertTransactional("completeRuleWindow", String.class, String.class,
LocalDateTime.class, LocalDateTime.class, long.class, long.class);
assertTransactional("failRuleWindow", String.class, String.class,
LocalDateTime.class, LocalDateTime.class, String.class, String.class);
assertTransactional("completeAlgorithmWindow", String.class, String.class,
LocalDateTime.class, LocalDateTime.class, long.class, long.class);
assertTransactional("failAlgorithmWindow", String.class, String.class,
LocalDateTime.class, LocalDateTime.class, String.class, String.class);
}
private void assertTransactional(String methodName, Class<?>... parameterTypes) throws Exception {
Method method = AnalysisCheckpointService.class.getMethod(methodName, parameterTypes);
Transactional annotation = method.getAnnotation(Transactional.class);
assertNotNull(annotation, methodName + " must commit when Hikari auto-commit is disabled");
assertTrue(annotation.rollbackFor().length == 0);
}
}
@@ -0,0 +1,26 @@
package com.common.service;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
class FactWriteOwnershipGuardTest {
@Test
void legacyModeKeepsRollbackWritePathAvailable() {
FactWriteOwnershipGuard guard = new FactWriteOwnershipGuard("LEGACY_CHANGE_LOG");
assertDoesNotThrow(() -> guard.assertLocalFactWriteAllowed("test"));
}
@Test
void directModesRejectWritesThatWouldBypassRuleTransaction() {
assertThrows(IllegalStateException.class,
() -> new FactWriteOwnershipGuard("DIRECT").assertLocalFactWriteAllowed("test"));
assertThrows(IllegalStateException.class,
() -> new FactWriteOwnershipGuard("DIRECT_SHADOW").assertLocalFactWriteAllowed("test"));
assertThrows(IllegalStateException.class,
() -> new FactWriteOwnershipGuard("FACT_ONLY_REBUILD_REQUIRED")
.assertLocalFactWriteAllowed("test"));
}
}
@@ -0,0 +1,17 @@
package com.common.service;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
class IncrementalStatsProjectionServiceSqlTest {
@Test
void pendingChangeJoinQualifiesColumnsPresentInBothTables() {
String sql = IncrementalStatsProjectionService.PENDING_CHANGE_QUERY;
assertTrue(sql.startsWith("SELECT c.change_seq,c.change_id,"));
assertTrue(sql.contains("JOIN syslog_stats_change_log c ON c.change_seq=p.change_seq"));
assertTrue(sql.contains("ORDER BY p.change_seq"));
}
}
@@ -0,0 +1,37 @@
package com.common.service;
import com.influx.InfluxDBClient;
import com.influx.raw.RawInfluxMetrics;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.mockito.Mockito.mock;
class InfluxSyslogServiceV2Test {
@Test void shanghaiMidnightChangesDailyMeasurement() {
assertEquals(Arrays.asList("syslog_security_20260813"), InfluxSyslogService.generateMeasurementNames(Instant.parse("2026-08-13T15:59:59.999Z"),Instant.parse("2026-08-13T16:00:00Z")));
assertEquals(Arrays.asList("syslog_security_20260814"), InfluxSyslogService.generateMeasurementNames(Instant.parse("2026-08-13T16:00:00Z"),Instant.parse("2026-08-13T16:00:00.001Z")));
}
@Test void dualReadPrefersV2AndRemovesInternalMarker() {
RawInfluxMetrics metrics=new RawInfluxMetrics(); InfluxSyslogService service=new InfluxSyslogService(mock(InfluxDBClient.class),metrics);
Map<String,Object> v1=row("V1","old",Instant.parse("2026-08-13T16:00:00.002Z")); Map<String,Object> v2=row("V2","new",Instant.parse("2026-08-13T16:00:00.001Z"));
List<Map<String,Object>> merged=service.merge(Arrays.asList(v1,v2)); assertEquals(1,merged.size()); assertEquals("new",merged.get(0).get("message")); assertFalse(merged.get(0).containsKey("__raw_schema")); assertEquals(1L,metrics.snapshot().get("influx_dual_read_duplicate_total")); assertEquals(1L,metrics.snapshot().get("influx_dual_read_conflict_total"));
}
@Test void duplicateV2FallbackPointsSelectEarliestTime() {
InfluxSyslogService service=new InfluxSyslogService(mock(InfluxDBClient.class),new RawInfluxMetrics());
Map<String,Object> later=row("V2","later",Instant.parse("2026-08-13T16:00:00.002Z"));
Map<String,Object> earlier=row("V2","earlier",Instant.parse("2026-08-13T16:00:00.001Z"));
assertEquals("earlier",service.merge(Arrays.asList(later,earlier)).get(0).get("message"));
}
private Map<String,Object> row(String schema,String message,Instant time){Map<String,Object> row=new LinkedHashMap<>();row.put("uuid","20260814-event");row.put("receive_time","20260814000000000");row.put("message",message);row.put("time",time);row.put("__raw_schema",schema);return row;}
}
@@ -0,0 +1,43 @@
package com.common.service;
import com.common.entity.IngressStatsEvent;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class IngressStatsProjectionWriterTest {
@Test
void aggregatesBatchByDeviceDateAndCollectorUsingStableMinMax() {
LocalDateTime t1 = LocalDateTime.of(2026, 8, 13, 10, 0);
LocalDateTime t2 = t1.plusMinutes(2);
IngressStatsProjectionWriter.Aggregation result = IngressStatsProjectionWriter.aggregate(Arrays.asList(
new IngressStatsEvent("a", 7L, 3L, t2),
new IngressStatsEvent("b", 7L, 3L, t1),
new IngressStatsEvent("a", 7L, 3L, t2),
new IngressStatsEvent("c", 8L, 4L, t1)));
assertEquals(2, result.devices.size());
assertEquals(2, result.collects.size());
assertEquals(t1, result.collects.get(3L).min);
assertEquals(t2, result.collects.get(3L).max);
}
@Test
void usesSameLockOrderRegardlessOfKafkaRecordOrder() {
LocalDateTime time = LocalDateTime.of(2026, 8, 13, 10, 0);
IngressStatsProjectionWriter.Aggregation result = IngressStatsProjectionWriter.aggregate(Arrays.asList(
new IngressStatsEvent("z", 9L, 7L, time),
new IngressStatsEvent("a", 2L, 3L, time),
new IngressStatsEvent("b", 8L, 4L, time.minusDays(1))));
List<Long> devices = new ArrayList<>();
for (IngressStatsProjectionWriter.DeviceKey key : result.devices.keySet()) devices.add(key.deviceId);
assertEquals(Arrays.asList(8L, 2L, 9L), devices);
assertEquals(Arrays.asList(3L, 4L, 7L), new ArrayList<>(result.collects.keySet()));
assertTrue(result.devices.size() > 1);
}
}
@@ -0,0 +1,74 @@
package com.common.service;
import com.common.entity.AnalysisAnalysisRule;
import com.common.entity.AnalysisGroupByColumn;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
class RealtimeAlarmIdentityTest {
@Test
void mutableAggregatesDoNotChangeGroupHashOrAlarmId() {
AnalysisGroupByColumn group = AnalysisGroupByColumn.builder()
.columnName("src_ip")
.build();
Map<String, Object> first = row("10.0.0.1", 1);
Map<String, Object> updated = row("10.0.0.1", 9);
AnalysisAnalysisRule rule = AnalysisAnalysisRule.builder()
.ruleId("rule-1")
.version(3)
.build();
RuleExecutionWindow window = new RuleExecutionWindow(
LocalDateTime.of(2026, 8, 12, 0, 55),
LocalDateTime.of(2026, 8, 12, 1, 0),
LocalDateTime.of(2026, 8, 12, 1, 2), "FIXED");
String firstHash = RealtimeAlarmIdentity.groupKeyHash(first, Arrays.asList(group));
String updatedHash = RealtimeAlarmIdentity.groupKeyHash(updated, Arrays.asList(group));
assertEquals(firstHash, updatedHash);
assertEquals(RealtimeAlarmIdentity.alarmId(rule, window, firstHash),
RealtimeAlarmIdentity.alarmId(rule, window, updatedHash));
}
@Test
void differentGroupOrWindowProducesDifferentIdentity() {
AnalysisGroupByColumn group = AnalysisGroupByColumn.builder().columnName("src_ip").build();
String firstHash = RealtimeAlarmIdentity.groupKeyHash(row("10.0.0.1", 1), Arrays.asList(group));
String secondHash = RealtimeAlarmIdentity.groupKeyHash(row("10.0.0.2", 1), Arrays.asList(group));
assertNotEquals(firstHash, secondHash);
AnalysisAnalysisRule rule = AnalysisAnalysisRule.builder().ruleId("rule-1").version(3).build();
RuleExecutionWindow first = new RuleExecutionWindow(
LocalDateTime.of(2026, 8, 12, 0, 55), LocalDateTime.of(2026, 8, 12, 1, 0),
LocalDateTime.of(2026, 8, 12, 1, 0), "FIXED");
RuleExecutionWindow second = new RuleExecutionWindow(
LocalDateTime.of(2026, 8, 12, 1, 0), LocalDateTime.of(2026, 8, 12, 1, 5),
LocalDateTime.of(2026, 8, 12, 1, 5), "FIXED");
assertNotEquals(RealtimeAlarmIdentity.alarmId(rule, first, firstHash),
RealtimeAlarmIdentity.alarmId(rule, second, firstHash));
}
@Test
void nullAndEmptyGroupValuesRemainDistinct() {
AnalysisGroupByColumn group = AnalysisGroupByColumn.builder().columnName("src_ip").build();
assertNotEquals(
RealtimeAlarmIdentity.groupKeyHash(row(null, 1), Arrays.asList(group)),
RealtimeAlarmIdentity.groupKeyHash(row("", 1), Arrays.asList(group)));
}
private Map<String, Object> row(String groupValue, int logCount) {
Map<String, Object> row = new HashMap<>();
row.put("__GROUP_KEY_0", groupValue);
row.put("log_count", logCount);
row.put("log_end_at", LocalDateTime.of(2026, 8, 12, 1, 0));
return row;
}
}
@@ -0,0 +1,86 @@
package com.common.service;
import com.common.entity.AnalysisGroupByWindow;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.assertEquals;
class RuleWindowCalculatorTest {
private final RuleWindowCalculator calculator = new RuleWindowCalculator();
@Test
void firstFixedWindowStartsAfterCutoverAndNeverReadsBackwards() {
AnalysisGroupByWindow config = AnalysisGroupByWindow.builder()
.windowType("TUMBLE")
.tumbleWindowSize(5)
.tumbleWindowSizeUnit("m")
.build();
RuleExecutionWindow window = calculator.firstWindowAfter(
LocalDateTime.of(2026, 8, 12, 1, 2), config);
assertEquals(LocalDateTime.of(2026, 8, 12, 1, 5), window.getWindowStart());
assertEquals(LocalDateTime.of(2026, 8, 12, 1, 10), window.getWindowEnd());
assertEquals("FIXED", window.getWindowType());
}
@Test
void arbitrarySevenMinuteWindowUsesStableContinuousAnchor() {
AnalysisGroupByWindow config = AnalysisGroupByWindow.builder()
.windowType("FIXED")
.tumbleWindowSize(7)
.tumbleWindowSizeUnit("m")
.build();
RuleExecutionWindow first = calculator.firstWindowAfter(
LocalDateTime.of(1970, 1, 1, 0, 57), config);
RuleExecutionWindow second = calculator.nextWindow(first, config);
assertEquals(LocalDateTime.of(1970, 1, 1, 1, 3), first.getWindowStart());
assertEquals(LocalDateTime.of(1970, 1, 1, 1, 10), first.getWindowEnd());
assertEquals(first.getWindowEnd(), second.getWindowStart());
assertEquals(LocalDateTime.of(1970, 1, 1, 1, 17), second.getWindowEnd());
}
@Test
void hopUsesSizeForRangeAndSlideForProgression() {
AnalysisGroupByWindow config = AnalysisGroupByWindow.builder()
.windowType("HOP")
.hopWindowSize(10)
.hopWindowSizeUnit("m")
.hopWindowSlide(5)
.hopWindowSlideUnit("m")
.build();
RuleExecutionWindow first = calculator.firstWindowAfter(
LocalDateTime.of(2026, 8, 12, 0, 52), config);
RuleExecutionWindow second = calculator.nextWindow(first, config);
assertEquals(LocalDateTime.of(2026, 8, 12, 0, 55), first.getWindowStart());
assertEquals(LocalDateTime.of(2026, 8, 12, 1, 5), first.getWindowEnd());
assertEquals(LocalDateTime.of(2026, 8, 12, 1, 0), second.getWindowStart());
assertEquals(LocalDateTime.of(2026, 8, 12, 1, 10), second.getWindowEnd());
assertEquals("SLIDING", first.getWindowType());
}
@Test
void latenessChangesFireTimeButNotBusinessBoundary() {
ReflectionTestUtils.setField(calculator, "allowedLatenessSeconds", 120L);
AnalysisGroupByWindow config = AnalysisGroupByWindow.builder()
.windowType("TUMBLE")
.tumbleWindowSize(5)
.tumbleWindowSizeUnit("m")
.build();
RuleExecutionWindow window = calculator.windowEndingAt(
LocalDateTime.of(2026, 8, 12, 1, 0), config);
assertEquals(LocalDateTime.of(2026, 8, 12, 0, 55), window.getWindowStart());
assertEquals(LocalDateTime.of(2026, 8, 12, 1, 0), window.getWindowEnd());
assertEquals(LocalDateTime.of(2026, 8, 12, 1, 2), window.getFireAt());
}
}
@@ -0,0 +1,69 @@
package com.common.service;
import com.common.entity.StatsChangeRecord;
import org.junit.jupiter.api.Test;
import java.time.LocalDate;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class StatsProjectionAccumulatorTest {
@Test
void countsEveryStandardizedPhysicalRowWithoutSyslogUuidDeduplication() {
StatsProjectionAccumulator accumulator = new StatsProjectionAccumulator();
LocalDate date = LocalDate.of(2026, 8, 13);
accumulator.accept(insert("NORMAL_DATA", "row-1", date, 7L, 11L));
accumulator.accept(insert("NORMAL_DATA", "row-2", date, 7L, 12L));
StatsProjectionAccumulator.DeviceDelta delta = accumulator.devices
.get(new StatsProjectionAccumulator.DailyKey(date, 7L));
assertEquals(2L, delta.parseDelta);
assertEquals(2, accumulator.rules.size(), "同一事件命中多规则时每个结果行都保留贡献");
}
@Test
void updateMovesContributionAndMarksOldMaxGroupDirty() {
StatsProjectionAccumulator accumulator = new StatsProjectionAccumulator();
LocalDate oldDate = LocalDate.of(2026, 8, 12);
LocalDate newDate = LocalDate.of(2026, 8, 13);
StatsChangeRecord update = StatsChangeRecord.builder()
.changeSeq(3L).changeId("u").sourceType("NORMAL_ALARM").sourceRowId("row")
.operationType("UPDATE").oldStatDate(oldDate).newStatDate(newDate)
.oldDeviceId(1L).newDeviceId(2L).oldRuleId(10L).newRuleId(20L)
.oldEventTime(oldDate.atTime(23, 59)).newEventTime(newDate.atTime(0, 1)).build();
accumulator.accept(update);
assertEquals(-1L, accumulator.devices.get(
new StatsProjectionAccumulator.DailyKey(oldDate, 1L)).parseDelta);
assertEquals(1L, accumulator.devices.get(
new StatsProjectionAccumulator.DailyKey(newDate, 2L)).parseDelta);
assertTrue(accumulator.dirty.containsKey(
new StatsProjectionAccumulator.DirtyKey("RULE", oldDate, 10L)));
}
@Test
void collectTaskUsesCreatedAtWhileDeviceUsesReceiveTime() {
StatsProjectionAccumulator accumulator = new StatsProjectionAccumulator();
LocalDateTime receiveTime = LocalDateTime.of(2026, 8, 13, 12, 0);
LocalDateTime createdAt = LocalDateTime.of(2026, 8, 13, 12, 1);
accumulator.accept(StatsChangeRecord.builder()
.changeSeq(1L).changeId("r").sourceType("DEVICE_RECEIVE").sourceRowId("1")
.operationType("INSERT").newStatDate(receiveTime.toLocalDate())
.newDeviceId(7L).newCollectId(9L).newEventTime(receiveTime)
.newCollectTime(createdAt).newPushSuccess(1).build());
assertEquals(receiveTime, accumulator.devices.get(
new StatsProjectionAccumulator.DailyKey(receiveTime.toLocalDate(), 7L)).maxReceiveTime);
assertEquals(createdAt, accumulator.collects.get(9L).lastSuccess);
}
private StatsChangeRecord insert(String source, String rowId, LocalDate date,
long deviceId, long ruleId) {
return StatsChangeRecord.builder().changeSeq(1L).changeId(rowId).sourceType(source)
.sourceRowId(rowId).operationType("INSERT").newStatDate(date)
.newDeviceId(deviceId).newRuleId(ruleId).newEventTime(date.atStartOfDay()).build();
}
}
@@ -0,0 +1,43 @@
package com.common.service;
import com.config.CacheConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.Acknowledgment;
import java.util.Collections;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
class SyslogCompensationServiceTest {
@Test
void legacyTypedDltEnvelopeCanBeIndexedAndAcknowledged() {
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
KafkaTemplate<String, String> kafkaTemplate = mock(KafkaTemplate.class);
Acknowledgment acknowledgment = mock(Acknowledgment.class);
SyslogCompensationService service = new SyslogCompensationService(
jdbcTemplate, kafkaTemplate, new CacheConfig().objectMapper());
String legacyEnvelope = "{\"@class\":\"com.Modules.NormalData.SyslogFailureRouter$FailureEnvelope\","
+ "\"envelopeType\":\"SYSLOG_RELIABILITY_V1\","
+ "\"eventId\":\"20260811-existing-event\",\"deviceId\":null,"
+ "\"originalTopic\":\"agent-01-syslog-topic.RETRY.1M\","
+ "\"originalPartition\":1,\"originalOffset\":576,"
+ "\"currentTopic\":\"agent-01-syslog-topic.RETRY.1M\","
+ "\"firstReceivedAt\":\"2026-08-11T10:08:58.696Z\","
+ "\"failedStage\":\"NORMALIZE\",\"errorCode\":\"SYSLOG_PERMANENT_DATA\","
+ "\"errorType\":\"java.lang.IllegalArgumentException\","
+ "\"errorMessage\":\"bad payload\",\"attemptCount\":1,"
+ "\"nextRetryAt\":null,\"ruleId\":null,\"ruleVersion\":null,"
+ "\"consumerVersion\":\"1.0.0\",\"originalPayload\":\"ciphertext\"}";
ConsumerRecord<String, String> record = new ConsumerRecord<>(
"agent-01-syslog-topic.DLT", 0, 985L, "event", legacyEnvelope);
service.indexDlt(Collections.singletonList(record), acknowledgment);
verify(acknowledgment).acknowledge();
}
}
@@ -0,0 +1,92 @@
package com.common.service;
import com.common.mapper.SyslogNormalAlarmMapper;
import com.common.mapper.SyslogNormalDataMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.test.util.ReflectionTestUtils;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
class SyslogNormalDataServiceIdempotencyTest {
private SyslogNormalDataMapper normalDataMapper;
private SyslogNormalAlarmMapper normalAlarmMapper;
private SyslogNormalDataService service;
@BeforeEach
void setUp() {
normalDataMapper = mock(SyslogNormalDataMapper.class);
normalAlarmMapper = mock(SyslogNormalAlarmMapper.class);
service = new SyslogNormalDataService();
ReflectionTestUtils.setField(service, "syslogNormalDataMapper", normalDataMapper);
ReflectionTestUtils.setField(service, "syslogNormalAlarmMapper", normalAlarmMapper);
ReflectionTestUtils.setField(service, "factWriteOwnershipGuard", mock(FactWriteOwnershipGuard.class));
}
@Test
void dmUniqueViolationWithExistingNormalResultIsIdempotentSuccess() {
Map<String, Object> data = idempotencyData(false);
when(normalDataMapper.insertDynamic(anyMap())).thenThrow(dmUniqueViolation());
when(normalDataMapper.countByEventRuleVersion("20260811-event", 7L, "legacy"))
.thenReturn(1);
assertEquals(0, service.insertDynamic(data));
verify(normalDataMapper).countByEventRuleVersion("20260811-event", 7L, "legacy");
verifyNoInteractions(normalAlarmMapper);
}
@Test
void dmUniqueViolationWithExistingAlarmResultIsIdempotentSuccess() {
Map<String, Object> data = idempotencyData(true);
when(normalAlarmMapper.insertDynamic(anyMap())).thenThrow(dmUniqueViolation());
when(normalAlarmMapper.countByEventRuleVersion("20260811-event", 7L, "legacy"))
.thenReturn(1);
assertEquals(0, service.insertDynamic(data));
verify(normalAlarmMapper).countByEventRuleVersion("20260811-event", 7L, "legacy");
verifyNoInteractions(normalDataMapper);
}
@Test
void nonUniqueIntegrityViolationIsStillRaised() {
Map<String, Object> data = idempotencyData(false);
DataIntegrityViolationException invalidData = new DataIntegrityViolationException(
"value too long", new SQLException("data too long"));
when(normalDataMapper.insertDynamic(anyMap())).thenThrow(invalidData);
assertThrows(DataIntegrityViolationException.class, () -> service.insertDynamic(data));
verifyNoInteractions(normalAlarmMapper);
}
private Map<String, Object> idempotencyData(boolean alarm) {
Map<String, Object> data = new HashMap<>();
data.put("syslog_uuid", "20260811-event");
data.put("normalize_rule_id", 7L);
data.put("rule_version", "legacy");
if (alarm) {
data.put("event_level", 1);
}
return data;
}
private DataIntegrityViolationException dmUniqueViolation() {
return new DataIntegrityViolationException(
"达梦写入失败",
new SQLException("Violate unique constraint on [syslog_normal_data_p_20260811]"));
}
}
@@ -0,0 +1,44 @@
package com.common.service;
import org.junit.jupiter.api.Test;
import org.springframework.boot.ApplicationArguments;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class WecomNotificationSequenceVerifierTest {
@Test
void verifierUsesReadOnlyMetadataQueriesAndDoesNotBlockStartup() {
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
when(jdbcTemplate.queryForObject(anyString(), eq(Long.class))).thenReturn(100L);
when(jdbcTemplate.query(anyString(), any(org.springframework.jdbc.core.RowMapper.class),
eq("SEQ_WECOM_NOTIFICATION"))).thenReturn(Collections.singletonList(50L));
WecomNotificationSequenceVerifier verifier = new WecomNotificationSequenceVerifier(jdbcTemplate);
assertDoesNotThrow(() -> verifier.run(mock(ApplicationArguments.class)));
verify(jdbcTemplate).queryForObject(anyString(), eq(Long.class));
verify(jdbcTemplate).query(anyString(), any(org.springframework.jdbc.core.RowMapper.class),
eq("SEQ_WECOM_NOTIFICATION"));
}
@Test
void metadataPermissionFailureDoesNotAbortApplicationStartup() {
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
when(jdbcTemplate.queryForObject(anyString(), eq(Long.class)))
.thenThrow(new IllegalStateException("USER_SEQUENCES denied"));
WecomNotificationSequenceVerifier verifier = new WecomNotificationSequenceVerifier(jdbcTemplate);
assertDoesNotThrow(() -> verifier.run(mock(ApplicationArguments.class)));
}
}
@@ -0,0 +1,36 @@
package com.common.service.impl;
import com.common.entity.AnalysisAnalysisRule;
import com.common.mapper.AnalysisAnalysisRuleMapper;
import com.common.service.AnalysisEngine;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
class AnalysisRuleServiceImplTest {
@Test
void internalFailedStatusMustNotBeWrappedAsSuccess() {
AnalysisRuleServiceImpl service = new AnalysisRuleServiceImpl();
AnalysisAnalysisRuleMapper mapper = mock(AnalysisAnalysisRuleMapper.class);
AnalysisEngine engine = mock(AnalysisEngine.class);
AnalysisAnalysisRule rule = AnalysisAnalysisRule.builder()
.ruleId("rule-1").ruleName("test").runMode("realtime").build();
when(mapper.selectByRuleId("rule-1")).thenReturn(rule);
Map<String, Object> failed = new HashMap<>();
failed.put("status", "failed");
when(engine.executeRules(anyList())).thenReturn(Collections.singletonList(failed));
ReflectionTestUtils.setField(service, "ruleMapper", mapper);
ReflectionTestUtils.setField(service, "realtimeAnalysisEngine", engine);
Map<String, Object> result = service.executeRealtimeRule("rule-1");
assertEquals(false, result.get("success"));
}
}
@@ -0,0 +1,66 @@
package com.common.service.impl;
import com.common.entity.AnalysisAnalysisRule;
import com.common.entity.AnalysisField;
import com.common.entity.AnalysisGroupByColumn;
import com.common.entity.AnalysisGroupByWindow;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ApplicationWindowSqlGeneratorTest {
@Test
void dmFixedWindowUsesBoundRangeWithoutDatabaseWindowFunction() {
SqlGeneratorServiceImplDm generator = new SqlGeneratorServiceImplDm();
AnalysisField field = AnalysisField.builder()
.columnName("src_ip")
.alarmColumnName("attack_ip")
.build();
AnalysisGroupByColumn group = AnalysisGroupByColumn.builder()
.columnName("src_ip")
.build();
AnalysisGroupByWindow window = AnalysisGroupByWindow.builder()
.windowType("TUMBLE")
.tumbleWindowSize(7)
.tumbleWindowSizeUnit("m")
.build();
String sql = generator.generateSql(AnalysisAnalysisRule.builder().ruleId("rule-1").build(),
Arrays.asList(field), Collections.emptyList(), Collections.emptyList(),
Arrays.asList(group), Collections.emptyList(), window,
"2026-08-12 00:56:00", "2026-08-12 01:03:00");
assertTrue(sql.contains("log_time >= ? AND log_time < ?"));
assertTrue(sql.contains("src_ip AS __group_key_0"));
assertFalse(sql.toUpperCase().contains("TUMBLE("));
assertFalse(sql.toUpperCase().contains("HOP("));
assertFalse(sql.contains("window_time"));
}
@Test
void dmSlidingWindowAlsoUsesOneApplicationSelectedRange() {
SqlGeneratorServiceImplDm generator = new SqlGeneratorServiceImplDm();
AnalysisField field = AnalysisField.builder()
.columnName("src_ip").alarmColumnName("attack_ip").build();
AnalysisGroupByColumn group = AnalysisGroupByColumn.builder().columnName("src_ip").build();
AnalysisGroupByWindow window = AnalysisGroupByWindow.builder()
.windowType("HOP")
.hopWindowSize(10).hopWindowSizeUnit("m")
.hopWindowSlide(5).hopWindowSlideUnit("m")
.build();
String sql = generator.generateSql(AnalysisAnalysisRule.builder().ruleId("rule-1").build(),
Arrays.asList(field), Collections.emptyList(), Collections.emptyList(),
Arrays.asList(group), Collections.emptyList(), window,
"2026-08-12 00:50:00", "2026-08-12 01:00:00");
assertTrue(sql.contains("log_time >= ? AND log_time < ?"));
assertFalse(sql.toUpperCase().contains("TUMBLE("));
assertFalse(sql.toUpperCase().contains("HOP("));
}
}
@@ -0,0 +1,50 @@
package com.common.service.impl;
import com.common.entity.Alarm;
import com.common.entity.AnalysisAnalysisRule;
import com.common.entity.AnalysisGroupByColumn;
import com.common.service.RuleExecutionWindow;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
class RealtimeAnalysisEngineDmMappingTest {
@Test
@SuppressWarnings("unchecked")
void applicationWindowDoesNotOverwriteRealLogEndTime() {
RealtimeAnalysisEngineDm engine = new RealtimeAnalysisEngineDm();
AnalysisAnalysisRule rule = AnalysisAnalysisRule.builder()
.ruleId("rule-1")
.ruleName("rule")
.version(1)
.build();
AnalysisGroupByColumn group = AnalysisGroupByColumn.builder()
.columnName("src_ip")
.build();
RuleExecutionWindow window = new RuleExecutionWindow(
LocalDateTime.of(2026, 8, 12, 0, 55),
LocalDateTime.of(2026, 8, 12, 1, 0),
LocalDateTime.of(2026, 8, 12, 1, 2), "FIXED");
Map<String, Object> row = new HashMap<>();
row.put("__group_key_0", "10.0.0.1");
row.put("log_start_at", LocalDateTime.of(2026, 8, 12, 0, 56));
row.put("log_end_at", LocalDateTime.of(2026, 8, 12, 0, 59, 59));
row.put("window_time", LocalDateTime.of(2026, 8, 12, 0, 55));
List<Alarm> alarms = ReflectionTestUtils.invokeMethod(engine, "convertToAlarms",
rule, Arrays.asList(row), window, Arrays.asList(group), true);
assertEquals(1, alarms.size());
assertEquals(LocalDateTime.of(2026, 8, 12, 0, 59, 59), alarms.get(0).getLogEndAt());
assertEquals(LocalDateTime.of(2026, 8, 12, 0, 55), alarms.get(0).getWindowStart());
assertEquals("2026-08-12 00:55:00", alarms.get(0).getWindowTime());
}
}
@@ -0,0 +1,37 @@
package com.common.service.impl;
import com.common.entity.AnalysisAnalysisRule;
import com.common.entity.AnalysisGroupByWindow;
import com.common.service.RuleWindowCalculator;
import org.junit.jupiter.api.Test;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.Duration;
import java.time.LocalDateTime;
import static org.mockito.Mockito.*;
class RuleExecutionTimeServiceImplTest {
@Test
void nextWindowIsAnchoredToCompletedWindowInsteadOfWallClock() {
RedisTemplate<String, String> redis = mock(RedisTemplate.class);
ValueOperations<String, String> values = mock(ValueOperations.class);
when(redis.opsForValue()).thenReturn(values);
RuleExecutionTimeServiceImpl service = new RuleExecutionTimeServiceImpl();
ReflectionTestUtils.setField(service, "redisTemplate", redis);
ReflectionTestUtils.setField(service, "ruleWindowCalculator", new RuleWindowCalculator());
AnalysisAnalysisRule rule = AnalysisAnalysisRule.builder()
.ruleId("rule-1").ruleName("rule").build();
AnalysisGroupByWindow window = AnalysisGroupByWindow.builder()
.windowType("TUMBLE").tumbleWindowSize(5).tumbleWindowSizeUnit("m").build();
service.updateNextExecuteTime(rule, window,
LocalDateTime.of(2026, 8, 7, 10, 0));
verify(values).set("rule:next_window_end:v2:rule-1", "2026-08-07 10:05:00", Duration.ofDays(5));
}
}
@@ -0,0 +1,68 @@
package com.config;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
class StatsIncrementalConfigurationVerifierTest {
@Test
void acceptsLegacyAndShadowModes() {
assertDoesNotThrow(() -> verifier("LEGACY_CHANGE_LOG", true,
false, false, true, false, true).validate());
assertDoesNotThrow(() -> verifier("DIRECT_SHADOW", true,
true, false, true, true, false).validate());
}
@Test
void acceptsIncrementalCutoverMode() {
assertDoesNotThrow(() -> verifier("LEGACY_CHANGE_LOG", true,
true, true, false, true, false).validate());
}
@Test
void acceptsDirectModeWithOldProjectorStopped() {
assertDoesNotThrow(() -> verifier("DIRECT", false,
false, true, false, true, false).validate());
}
@Test
void rejectsRawFactConsumerOutsideLegacyMode() {
assertThrows(IllegalStateException.class, () -> new StatsIncrementalConfigurationVerifier(
"DIRECT", false, false, true, false, true, false, true).validate());
assertThrows(IllegalStateException.class, () -> new StatsIncrementalConfigurationVerifier(
"DIRECT_SHADOW", true, true, false, true, true, false, true).validate());
}
@Test
void rejectsDualWriterAndNoWriterModes() {
assertThrows(IllegalStateException.class,
() -> verifier("LEGACY_CHANGE_LOG", true,
true, true, true, true, false).validate());
assertThrows(IllegalStateException.class,
() -> verifier("LEGACY_CHANGE_LOG", true,
true, false, false, true, false).validate());
assertThrows(IllegalStateException.class,
() -> verifier("LEGACY_CHANGE_LOG", true,
false, true, false, true, false).validate());
assertThrows(IllegalStateException.class,
() -> verifier("LEGACY_CHANGE_LOG", true,
true, false, true, false, false).validate());
assertThrows(IllegalStateException.class,
() -> verifier("LEGACY_CHANGE_LOG", true,
true, true, false, false, true).validate());
assertThrows(IllegalStateException.class,
() -> verifier("DIRECT", true,
false, true, false, true, false).validate());
assertThrows(IllegalStateException.class,
() -> verifier("DIRECT", false,
true, true, false, true, false).validate());
}
private StatsIncrementalConfigurationVerifier verifier(
String mode, boolean capture, boolean projection, boolean read,
boolean legacy, boolean ingress, boolean legacyReceive) {
return new StatsIncrementalConfigurationVerifier(mode, capture, projection, read,
legacy, ingress, legacyReceive, false);
}
}
@@ -0,0 +1,111 @@
package com.influx.raw;
import com.influx.InfluxDBClient;
import com.common.service.InfluxSyslogService;
import com.influxdb.client.domain.WritePrecision;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.Instant;
import java.util.HashSet;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class InfluxRawV2ContractTest {
@Test void fixedBucketVectorsMatchProtocol() {
assertEquals("1269", InfluxEventBucket.bucketOf("20260814-00000000-0000-0000-0000-000000000000"));
assertEquals("2085", InfluxEventBucket.bucketOf("20260814-a3d874a0-6f50-4f0c-9e7a-9c20303360f5"));
assertThrows(IllegalArgumentException.class, () -> InfluxEventBucket.bucketOf(" uuid"));
}
@Test void configurationRejectsChangingPersistentBucketCount() {
RawStorageProperties properties=new RawStorageProperties();
ReflectionTestUtils.setField(properties,"writeSchemaValue","V2");
ReflectionTestUtils.setField(properties,"readModeValue","V1_V2");
ReflectionTestUtils.setField(properties,"v1Bucket","yelangbucket");
ReflectionTestUtils.setField(properties,"v2Bucket","yelangbucket_v2");
ReflectionTestUtils.setField(properties,"uuidBucketCount",1024);
ReflectionTestUtils.setField(properties,"queryMaxRows",10000);
ReflectionTestUtils.setField(properties,"queryMaxResponseBytes",10485760L);
assertThrows(IllegalArgumentException.class,properties::validate);
}
@Test void firstMillionSuffixesAreUniqueAndThenCycle() {
PointTimeAllocator allocator = new PointTimeAllocator(); long millis = 1_786_686_912_123L;
Set<Long> suffixes = new HashSet<>(1_333_334);
for (int i=0;i<1_000_000;i++) { long value=allocator.allocate(millis); assertEquals(millis,Math.floorDiv(value,1_000_000L)); assertTrue(suffixes.add(Math.floorMod(value,1_000_000L))); }
assertEquals(0L, Math.floorMod(allocator.allocate(millis),1_000_000L));
}
@Test void concurrentAllocationUsesAtomicSequence() throws Exception {
PointTimeAllocator allocator=new PointTimeAllocator(); Set<Long> values=ConcurrentHashMap.newKeySet(); ExecutorService pool=Executors.newFixedThreadPool(8);
for(int i=0;i<100_000;i++) pool.submit(()->values.add(allocator.allocate(1_786_686_912_123L)));
pool.shutdown(); assertTrue(pool.awaitTermination(20, TimeUnit.SECONDS)); assertEquals(100_000,values.size());
}
@Test void overflowIsNotSilentlyReplacedWithCurrentTime() {
assertThrows(ArithmeticException.class,()->new PointTimeAllocator().allocate(Long.MAX_VALUE));
}
@Test void receiveTimeAlwaysUsesShanghaiAndOneParsedValue() {
TimeZone original=TimeZone.getDefault();
try { TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles")); ReceiveTimeParser.ParsedReceiveTime parsed=ReceiveTimeParser.parse("20260814000000000"); assertEquals(Instant.parse("2026-08-13T16:00:00Z").toEpochMilli(),parsed.getEpochMillis()); assertEquals("20260814",parsed.getMeasurementDate()); }
finally { TimeZone.setDefault(original); }
}
@Test void v2HasUuidFieldButNeverUuidTagAndUsesNanoseconds() {
RawStorageProperties properties=mock(RawStorageProperties.class); when(properties.getWriteSchema()).thenReturn(RawStorageProperties.WriteSchema.V2); when(properties.getV2Bucket()).thenReturn("yelangbucket_v2");
InfluxRawPointFactory.RawPoint raw=new InfluxRawPointFactory(properties).create("20260814-a3d874a0-6f50-4f0c-9e7a-9c20303360f5","message","20260814000000000","126","1","agent-01-syslog-topic",ReceiveTimeParser.parse("20260814000000000"),1_786_636_800_000_000_123L);
String line=raw.getPoint().toLineProtocol(); assertFalse(line.substring(0,line.indexOf(' ')).contains("uuid=")); assertTrue(line.contains("event_bucket=2085")); assertTrue(line.contains("uuid=\"20260814-a3d874a0-6f50-4f0c-9e7a-9c20303360f5\"")); assertEquals(WritePrecision.NS,raw.getPoint().getPrecision()); assertEquals("yelangbucket_v2",raw.getBucket());
}
@Test void onlyMissingPointTimeFallsBack() {
RawStorageProperties properties=mock(RawStorageProperties.class); when(properties.isLegacyFallbackEnabled()).thenReturn(true); RawInfluxMetrics metrics=new RawInfluxMetrics(); PointTimeResolver resolver=new PointTimeResolver(properties,metrics); long millis=ReceiveTimeParser.parse("20260814000000000").getEpochMillis();
assertNotNull(resolver.resolve(false,null,millis,true)); assertThrows(IllegalArgumentException.class,()->resolver.resolve(true,"-1",millis,true)); assertThrows(IllegalArgumentException.class,()->resolver.resolve(true,"not-number",millis,true)); assertThrows(IllegalArgumentException.class,()->resolver.resolve(true,Long.toString(millis*1_000_000L+1_000_000L),millis,true)); assertEquals(1L,metrics.snapshot().get("legacy_missing_point_time_total"));
}
@Test void disabledFallbackStillCountsMissingAndFailure() {
RawStorageProperties properties=mock(RawStorageProperties.class);
RawInfluxMetrics metrics=new RawInfluxMetrics();
PointTimeResolver resolver=new PointTimeResolver(properties,metrics);
assertThrows(IllegalArgumentException.class,
()->resolver.resolve(false,null,1_786_636_800_000L,true));
assertEquals(1L,metrics.snapshot().get("legacy_missing_point_time_total"));
assertEquals(1L,metrics.snapshot().get("legacy_point_time_fallback_failed_total"));
}
@Test void mixedLegacyAndNewMessagesAllProduceDistinctV2PointsAndRetryIsIdempotent() {
RawStorageProperties properties=mock(RawStorageProperties.class); when(properties.isLegacyFallbackEnabled()).thenReturn(true); when(properties.getWriteSchema()).thenReturn(RawStorageProperties.WriteSchema.V2); when(properties.getV2Bucket()).thenReturn("yelangbucket_v2");
RawInfluxMetrics metrics=new RawInfluxMetrics(); PointTimeResolver resolver=new PointTimeResolver(properties,metrics); InfluxRawPointFactory factory=new InfluxRawPointFactory(properties); ReceiveTimeParser.ParsedReceiveTime parsed=ReceiveTimeParser.parse("20260814000000000");
long first=resolver.resolve(false,null,parsed.getEpochMillis(),true); long second=resolver.resolve(false,null,parsed.getEpochMillis(),true); assertNotEquals(first,second);
String id="20260814-a3d874a0-6f50-4f0c-9e7a-9c20303360f5"; String originalTopic="agent-01-syslog-topic";
String firstLine=factory.create(id,"message","20260814000000000","126","1",originalTopic,parsed,first).getPoint().toLineProtocol();
String retryLine=factory.create(id,"message","20260814000000000","126","1",originalTopic,parsed,first).getPoint().toLineProtocol();
String secondLine=factory.create("20260814-00000000-0000-0000-0000-000000000000","message2","20260814000000000","126","1",originalTopic,parsed,second).getPoint().toLineProtocol();
assertEquals(firstLine,retryLine); assertNotEquals(firstLine,secondLine); assertTrue(firstLine.contains("topic=agent-01-syslog-topic")); assertFalse(firstLine.contains("RETRY"));
}
@Test void fluxEscapingPreventsLiteralAndControlCharacterInjection() {
assertEquals("a\\\"b\\\\c", InfluxDBClient.escapeFluxString("a\"b\\c"));
assertThrows(IllegalArgumentException.class,()->InfluxDBClient.escapeFluxString("x\u0001y"));
}
@Test void uuidCandidateFluxComputesBucketEscapesUuidAndSelectsEarliestGlobally() {
String uuid="20260814-a\" |> die() //";
String query=InfluxDBClient.buildUuidV2CandidateQuery("yelangbucket_v2",
"syslog_security_20260814",uuid,Instant.parse("2026-08-13T16:00:00Z"),
Instant.parse("2026-08-14T16:00:00Z"));
assertTrue(query.contains("r.event_bucket == \""+InfluxEventBucket.bucketOf(uuid)+"\""));
assertTrue(query.contains("r._value == \"20260814-a\\\" |> die() //\""));
assertTrue(query.indexOf("|> group()") < query.indexOf("|> sort("));
assertTrue(query.indexOf("|> sort(") < query.indexOf("|> limit(n: 2)"));
}
}