feat(syslog): 完成高吞吐可靠链路与统计存储优化
- 新增接入 WAL、检查点、背压恢复及可靠分发机制,完善 Kafka 幂等发布、分区、重试、DLT 和补偿流程 - 引入规则预编译、批内快照及 JetCache/Caffeine 两级缓存,支持缓存失效广播、影子对账和执行指标 - 升级 InfluxDB V2 原始日志模型,统一接收时间与 Point 时间,支持按日 measurement、UUID 分桶及 V1/V2 双读 - 重构达梦标准化批量写入与统计链路,支持事务内直写、增量投影、幂等控制、窗口水位和日期切换 - 完善告警处理、配置校验、运行监控及测试覆盖
This commit is contained in:
+60
@@ -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);
|
||||
}
|
||||
}
|
||||
+23
@@ -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"));
|
||||
}
|
||||
}
|
||||
+28
@@ -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));
|
||||
}
|
||||
}
|
||||
+149
@@ -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());
|
||||
}
|
||||
}
|
||||
+28
@@ -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());
|
||||
}
|
||||
}
|
||||
+19
@@ -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");
|
||||
}
|
||||
}
|
||||
+84
@@ -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());
|
||||
}
|
||||
}
|
||||
+38
@@ -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());
|
||||
}
|
||||
}
|
||||
+112
@@ -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"));
|
||||
}
|
||||
}
|
||||
+55
@@ -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);
|
||||
}
|
||||
}
|
||||
+26
@@ -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"));
|
||||
}
|
||||
}
|
||||
+17
@@ -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"));
|
||||
}
|
||||
}
|
||||
+37
@@ -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;}
|
||||
}
|
||||
+43
@@ -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);
|
||||
}
|
||||
}
|
||||
+74
@@ -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;
|
||||
}
|
||||
}
|
||||
+86
@@ -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());
|
||||
}
|
||||
}
|
||||
+69
@@ -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();
|
||||
}
|
||||
}
|
||||
+43
@@ -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();
|
||||
}
|
||||
}
|
||||
+92
@@ -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]"));
|
||||
}
|
||||
}
|
||||
+44
@@ -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)));
|
||||
}
|
||||
}
|
||||
+36
@@ -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"));
|
||||
}
|
||||
}
|
||||
+66
@@ -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("));
|
||||
}
|
||||
}
|
||||
+50
@@ -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());
|
||||
}
|
||||
}
|
||||
+37
@@ -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));
|
||||
}
|
||||
}
|
||||
+68
@@ -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);
|
||||
}
|
||||
}
|
||||
+111
@@ -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)"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user