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,74 @@
package com.Modules.NormalData;
import com.Modules.NormalData.compiled.CompiledRule;
import com.Modules.NormalData.compiled.CompiledRuleCompiler;
import com.Modules.NormalData.compiled.RuleSource;
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 com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
class CompiledRuleExecutionEquivalenceTest {
private final CompiledRuleCompiler compiler = new CompiledRuleCompiler(new ObjectMapper());
private final LogNormalProcessor legacy = new LogNormalProcessor(mock(DmNormalizeRuleService.class),
mock(DmColumnService.class), mock(SyslogNormalDataService.class),
mock(SyslogNonNormalMessageService.class), new LogDataFilterService(),
new LogDataCompleteService(), mock(DeviceCollectTaskService.class),
mock(DeviceDeviceService.class), mock(DmColumnMapper.class), mock(DmNormalizeRuleMapper.class),
mock(KafkaConsumerProperties.class), mock(AppProperties.class));
@Test
void allExistingMessageParsersProduceTheSameCompleteFieldMap() {
List<Fixture> fixtures = Arrays.asList(
new Fixture("json", "<14> host {\"a\":1,\"nested\":{\"b\":\"x\"}}", "{"
+ "\"cropper_params\":{\"head_key\":\"{\",\"head_offset\":0,\"tail_key\":\"\",\"tail_offset\":0},"
+ "\"mappers\":[]}"),
new Fixture("kv", "a=1 b=two", "{"
+ "\"cropper_params\":{\"head_key\":\"a\",\"head_offset\":0,\"tail_key\":\"\",\"tail_offset\":0},"
+ "\"kv_params\":{\"internal\":\"=\",\"external\":\" \",\"l_trim\":\"\",\"r_trim\":\"\"},"
+ "\"mappers\":[]}"),
new Fixture("sep", "x,y,z", "{"
+ "\"cropper_params\":{\"head_key\":\"x\",\"head_offset\":0,\"tail_key\":\"\",\"tail_offset\":0},"
+ "\"sep\":\",\",\"mappers\":[]}"),
new Fixture("regex", "src=10.0.0.1 dst=10.0.0.2", "{"
+ "\"cropper_params\":{\"head_key\":\"src\",\"head_offset\":0,\"tail_key\":\"\",\"tail_offset\":0},"
+ "\"regexp\":\"src=(\\\\S+) dst=(\\\\S+)\",\"mappers\":[]}"));
long id = 1L;
for (Fixture fixture : fixtures) {
RuleSource source = new RuleSource(id++, "fixture", "fixture", fixture.type,
fixture.content, true, "0");
LinkedHashMap<String, Object> interpreted = legacy.getMessageToMap(
source.asLegacyMap(), fixture.message, fixture.type);
CompiledRule compiled = compiler.compile(source);
assertEquals(interpreted, compiled.parse(fixture.message).getFields(), fixture.type);
}
}
private static final class Fixture {
private final String type;
private final String message;
private final String content;
private Fixture(String type, String message, String content) {
this.type = type;
this.message = message;
this.content = content;
}
}
}
@@ -0,0 +1,158 @@
package com.Modules.NormalData;
import com.Modules.NormalData.compiled.BatchEnrichmentContext;
import com.Modules.NormalData.compiled.BatchRuleSnapshot;
import com.Modules.NormalData.compiled.CompiledRuleCache;
import com.Modules.NormalData.compiled.CompiledRuleCompiler;
import com.Modules.NormalData.compiled.RuleExecutionMetrics;
import com.Modules.NormalData.compiled.RuleSource;
import com.common.entity.DeviceCollectTask;
import com.common.entity.DeviceDevice;
import com.common.mapper.DmColumnMapper;
import com.common.mapper.DmNormalizeRuleMapper;
import com.common.service.*;
import com.config.AppProperties;
import com.config.KafkaConsumerProperties;
import com.config.NormalizationOptimizationProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class LogNormalProcessorCompiledEquivalenceTest {
@Test
void compiledAndLegacyExecutorsProduceDeeplyEqualRowsAndReuseMessageParsing() {
RuleSource first = rule(31L, "rule-one");
RuleSource second = rule(32L, "rule-two");
List<RuleSource> rules = Arrays.asList(first, second);
Dependencies dependencies = dependencies(rules);
NormalizationOptimizationProperties legacyConfig = config(false, false);
NormalizationOptimizationProperties compiledConfig = config(true, true);
LogNormalProcessor legacy = processor(dependencies, legacyConfig);
LogNormalProcessor compiled = processor(dependencies, compiledConfig);
Map<Long, List<RuleSource>> byDevice = new LinkedHashMap<>();
byDevice.put(7L, rules);
BatchRuleSnapshot snapshot = new BatchRuleSnapshot(byDevice, System.currentTimeMillis());
LocalDateTime receivedAt = LocalDateTime.of(2026, 8, 14, 10, 30);
String message = "[receive_time=20260814103000000 event_id=event-1 device_id=7 "
+ "device_name=fw vendor=vendor data_type=json device_collect_id=3]"
+ "<14>1 host app - - - {\"src_ip\":\"10.2.3.4\",\"level\":\"high\"}";
LogNormalProcessor.NormalizationResult legacyResult = legacy.prepare(message, "event-1", "raw",
receivedAt, snapshot, new BatchEnrichmentContext(false,
dependencies.taskService, dependencies.deviceService), new RuleExecutionMetrics());
RuleExecutionMetrics metrics = new RuleExecutionMetrics();
LogNormalProcessor.NormalizationResult compiledResult = compiled.prepare(message, "event-1", "raw",
receivedAt, snapshot, new BatchEnrichmentContext(true,
dependencies.taskService, dependencies.deviceService), metrics);
assertEquals(legacyResult.getOutcome(), compiledResult.getOutcome());
assertEquals(legacyResult.getRows(), compiledResult.getRows());
assertEquals(1, metrics.getParseMisses());
assertEquals(1, metrics.getParseHits());
assertEquals(2, metrics.getCompiledMisses());
}
@Test
void incompatibleRuleFallsBackToLegacyWithoutFailingTheMessage() {
RuleSource legacyOnly = legacyOnlyRule(1989234824495550465L, "legacy-only");
List<RuleSource> rules = Collections.singletonList(legacyOnly);
Dependencies dependencies = dependencies(rules);
LogNormalProcessor legacy = processor(dependencies, config(false, false));
LogNormalProcessor compiled = processor(dependencies, config(true, true));
Map<Long, List<RuleSource>> byDevice = new LinkedHashMap<>();
byDevice.put(7L, rules);
BatchRuleSnapshot snapshot = new BatchRuleSnapshot(byDevice, System.currentTimeMillis());
LocalDateTime receivedAt = LocalDateTime.of(2026, 8, 15, 0, 21);
String message = "[receive_time=20260815002100000 event_id=event-fallback device_id=7 "
+ "device_name=fw vendor=vendor data_type=json device_collect_id=3]"
+ "<14>1 host app - - - {\"src_ip\":\"10.2.3.4\"}";
LogNormalProcessor.NormalizationResult expected = legacy.prepare(message, "event-fallback", "raw",
receivedAt, snapshot, new BatchEnrichmentContext(false,
dependencies.taskService, dependencies.deviceService), new RuleExecutionMetrics());
LogNormalProcessor.NormalizationResult actual = compiled.prepare(message, "event-fallback", "raw",
receivedAt, snapshot, new BatchEnrichmentContext(true,
dependencies.taskService, dependencies.deviceService), new RuleExecutionMetrics());
assertEquals(expected.getOutcome(), actual.getOutcome());
assertEquals(expected.getRows(), actual.getRows());
}
private static RuleSource rule(long id, String name) {
String content = "{"
+ "\"cropper_params\":{\"head_key\":\"{\",\"head_offset\":0,\"tail_key\":\"\",\"tail_offset\":0},"
+ "\"mappers\":["
+ "{\"origin_field\":\"src_ip\",\"dest_field\":\"src_ip\",\"action\":{\"type\":\"equal\",\"param\":{}}},"
+ "{\"origin_field\":\"level\",\"dest_field\":\"severity\",\"action\":{\"type\":\"mapping\",\"param\":{\"mapping\":{\"high\":3}}}}],"
+ "\"filters_params\":{\"expressions\":[]},"
+ "\"complete_columns\":[{\"column_name\":\"country\",\"value\":\"CN\","
+ "\"filters_relation\":\"and\",\"conditions\":[{\"column_name\":\"src_ip\","
+ "\"op\":\"regexp\",\"value\":\"10\\\\..*\"}]}]}";
return new RuleSource(id, name, name, "json", content, true, "0");
}
private static RuleSource legacyOnlyRule(long id, String name) {
String content = "{"
+ "\"cropper_params\":{\"head_key\":\"{\",\"head_offset\":0,\"tail_key\":\"\",\"tail_offset\":0},"
+ "\"mappers\":[{\"origin_field\":\"src_ip\",\"dest_field\":\"src_ip\","
+ "\"action\":{\"type\":\"legacy_custom\",\"param\":{}}}],"
+ "\"filters_params\":{\"expressions\":[]},\"complete_columns\":[]}";
return new RuleSource(id, name, name, "json", content, true, "0");
}
private static NormalizationOptimizationProperties config(boolean compiled, boolean parseCache) {
NormalizationOptimizationProperties properties = new NormalizationOptimizationProperties();
properties.getCompiledRule().setEnabled(compiled);
properties.getCompiledRule().setShadowCompareEnabled(false);
properties.getMessageParseCache().setEnabled(parseCache);
properties.getBatchEnrichmentCache().setEnabled(compiled);
return properties;
}
private static LogNormalProcessor processor(Dependencies dependencies,
NormalizationOptimizationProperties properties) {
CompiledRuleCompiler compiler = new CompiledRuleCompiler(new ObjectMapper());
return new LogNormalProcessor(dependencies.ruleService, mock(DmColumnService.class),
mock(SyslogNormalDataService.class), mock(SyslogNonNormalMessageService.class),
new LogDataFilterService(), new LogDataCompleteService(), dependencies.taskService,
dependencies.deviceService, mock(DmColumnMapper.class), mock(DmNormalizeRuleMapper.class),
mock(KafkaConsumerProperties.class), mock(AppProperties.class),
new CompiledRuleCache(compiler, properties), properties);
}
private static Dependencies dependencies(List<RuleSource> rules) {
Dependencies dependencies = new Dependencies();
dependencies.ruleService = mock(DmNormalizeRuleService.class);
dependencies.taskService = mock(DeviceCollectTaskService.class);
dependencies.deviceService = mock(DeviceDeviceService.class);
when(dependencies.ruleService.selectByDeviceIdAuto(7L)).thenReturn(rules);
DeviceDevice device = new DeviceDevice();
device.setIp("192.0.2.7");
device.setVendor("vendor");
device.setName("firewall");
when(dependencies.deviceService.getByIdSafely(7)).thenReturn(device);
DeviceCollectTask task = new DeviceCollectTask();
task.setDeviceIp("192.0.2.3");
task.setTaskName("agent-three");
when(dependencies.taskService.getById(3)).thenReturn(task);
return dependencies;
}
private static final class Dependencies {
private DmNormalizeRuleService ruleService;
private DeviceCollectTaskService taskService;
private DeviceDeviceService deviceService;
}
}
@@ -0,0 +1,67 @@
package com.Modules.NormalData;
import com.Modules.NormalData.compiled.BatchEnrichmentContext;
import com.Modules.NormalData.compiled.BatchRuleSnapshot;
import com.Modules.NormalData.compiled.RuleExecutionMetrics;
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";
BatchRuleSnapshot snapshot = processor.captureRuleSnapshot(Collections.singleton(127L));
BatchEnrichmentContext enrichment = new BatchEnrichmentContext(false, collectTaskService, deviceService);
assertEquals(LogNormalProcessor.ProcessingOutcome.FILTERED,
processor.prepare(message, "20260811-test", "agent-01-syslog-topic", null,
snapshot, enrichment, new RuleExecutionMetrics()).getOutcome());
assertEquals(LogNormalProcessor.ProcessingOutcome.FILTERED,
processor.prepare(message, "20260811-test-2", "agent-01-syslog-topic", null,
snapshot, enrichment, new RuleExecutionMetrics()).getOutcome());
verify(ruleService, times(1)).selectByDeviceIdAuto(127L);
verifyNoInteractions(normalDataService, nonNormalMessageService, filterService, completeService);
}
}
@@ -0,0 +1,69 @@
package com.Modules.NormalData;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
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"));
}
@Test
void retryUsesStableKafkaFallbackWhenHeaderReceiveTimeIsMissing() {
LocalDateTime fallback = LocalDateTime.of(2026, 8, 11, 3, 55, 57, 109_000_000);
assertEquals(fallback, LogNormalProcessor.resolveInitialReceivedAt(null, fallback));
assertEquals(fallback, LogNormalProcessor.resolveInitialReceivedAt("invalid", fallback));
}
@Test
void initialReceiveTimeSetsBothPartitionAndCreationTime() {
LocalDateTime receiveTime = LocalDateTime.of(2026, 8, 11, 3, 55, 57, 109_000_000);
Map<String, Object> columns = new HashMap<>();
LogNormalProcessor.applyInitialReceiveTime(columns, receiveTime);
assertEquals(receiveTime, columns.get("log_time"));
assertEquals(receiveTime, columns.get("created_at"));
}
@Test
void ruleProducedLogTimeCannotChangeReceiveDayPartition() {
LocalDateTime eventTime = LocalDateTime.of(2026, 8, 10, 20, 11, 24, 736_717_000);
LocalDateTime receiveTime = LocalDateTime.of(2026, 8, 11, 12, 30, 44, 199_000_000);
Map<String, Object> columns = new HashMap<>();
columns.put("log_time", eventTime);
LogNormalProcessor.applyInitialReceiveTime(columns, receiveTime);
assertEquals(receiveTime, columns.get("log_time"));
assertEquals(receiveTime, columns.get("created_at"));
}
@Test
void deterministicResultIdIsStableAndKeepsUuidLength() {
String first = LogNormalProcessor.deterministicResultId("20260811-event", 7L, "legacy");
String retry = LogNormalProcessor.deterministicResultId("20260811-event", 7L, "legacy");
String anotherRule = LogNormalProcessor.deterministicResultId("20260811-event", 8L, "legacy");
assertEquals(first, retry);
assertEquals(36, first.length());
assertNotEquals(first, anotherRule);
}
}
@@ -0,0 +1,52 @@
package com.Modules.NormalData;
import com.Modules.NormalData.compiled.NormalizationShadowComparator;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class NormalizationShadowComparatorTest {
private final NormalizationShadowComparator comparator = new NormalizationShadowComparator();
@Test
void comparesCanonicalNumbersAndSortsRowsByStableId() {
Map<String, Object> one = row("b", 1);
Map<String, Object> two = row("a", new BigDecimal("2.00"));
Map<String, Object> oneCandidate = row("b", new BigDecimal("1.0"));
Map<String, Object> twoCandidate = row("a", 2L);
LogNormalProcessor.NormalizationResult left = LogNormalProcessor.NormalizationResult.success(
Arrays.asList(one, two));
LogNormalProcessor.NormalizationResult right = LogNormalProcessor.NormalizationResult.success(
Arrays.asList(twoCandidate, oneCandidate));
assertTrue(comparator.compare(left, right).isMatches());
}
@Test
void distinguishesMissingAndNullAndUsesStableSampling() {
Map<String, Object> missing = row("a", 1);
Map<String, Object> withNull = row("a", 1);
withNull.put("nullable", null);
NormalizationShadowComparator.Diff diff = comparator.compare(
LogNormalProcessor.NormalizationResult.success(Collections.singletonList(missing)),
LogNormalProcessor.NormalizationResult.success(Collections.singletonList(withNull)));
assertFalse(diff.isMatches());
assertEquals(Collections.singleton("nullable"), diff.getFields());
assertEquals(comparator.sampled("event-42", 0.25), comparator.sampled("event-42", 0.25));
assertEquals(12, comparator.eventHash("event-42").length());
}
private static Map<String, Object> row(String id, Object value) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("id", id);
row.put("value", value);
return row;
}
}
@@ -0,0 +1,148 @@
package com.Modules.NormalData;
import com.Modules.NormalData.compiled.BatchEnrichmentContext;
import com.Modules.NormalData.compiled.BatchRuleSnapshot;
import com.Modules.NormalData.compiled.PreparedNormalizationInput;
import com.common.service.DeviceCollectTaskService;
import com.common.service.DeviceDeviceService;
import com.common.service.DmStandardizedBatchWriter;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/** 验证恢复并行后仍保留“全部可靠终态才允许返回”的处理边界。 */
class SysLogProcessorParallelTest {
private ExecutorService executor;
private SysLogProcessor processor;
private LogNormalProcessor normalProcessor;
private SyslogFailureRouter failureRouter;
private DmStandardizedBatchWriter batchWriter;
@BeforeEach
void setUp() {
executor = Executors.newFixedThreadPool(4);
processor = new SysLogProcessor();
normalProcessor = mock(LogNormalProcessor.class);
failureRouter = mock(SyslogFailureRouter.class);
batchWriter = mock(DmStandardizedBatchWriter.class);
ReflectionTestUtils.setField(processor, "taskExecutor", executor);
ReflectionTestUtils.setField(processor, "processTimeoutMs", 5000L);
ReflectionTestUtils.setField(processor, "logNormalProcessor", normalProcessor);
ReflectionTestUtils.setField(processor, "failureRouter", failureRouter);
ReflectionTestUtils.setField(processor, "standardizedBatchWriter", batchWriter);
when(batchWriter.write(any())).thenReturn(new DmStandardizedBatchWriter.WriteReport<>());
when(normalProcessor.captureRuleSnapshot(any())).thenReturn(
new BatchRuleSnapshot(Collections.emptyMap(), System.currentTimeMillis()));
when(normalProcessor.createBatchEnrichmentContext()).thenReturn(new BatchEnrichmentContext(
false, mock(DeviceCollectTaskService.class), mock(DeviceDeviceService.class)));
}
@AfterEach
void tearDown() throws InterruptedException {
executor.shutdownNow();
executor.awaitTermination(5, TimeUnit.SECONDS);
}
@Test
void standardizationRunsWithBoundedParallelism() {
AtomicInteger active = new AtomicInteger();
AtomicInteger maxActive = new AtomicInteger();
when(normalProcessor.prepare(any(PreparedNormalizationInput.class),
any(), any(), any())).thenAnswer(invocation -> {
int current = active.incrementAndGet();
maxActive.accumulateAndGet(current, Math::max);
try {
Thread.sleep(80L);
return LogNormalProcessor.NormalizationResult.success(Collections.singletonList(normalizedRow()));
} finally {
active.decrementAndGet();
}
});
SysLogProcessor.BatchMetrics metrics = new SysLogProcessor.BatchMetrics();
processor.normalizePreparedMessages(messages(8), metrics);
assertTrue(maxActive.get() > 1, "批内标准化应并行执行");
assertTrue(maxActive.get() <= 4, "并行度不得超过有界线程池");
assertTrue(metrics.successCount() == 8, "全部成功消息都应计入可靠终态");
}
@Test
void systemicFailureIsNotSwallowed() {
when(normalProcessor.prepare(any(PreparedNormalizationInput.class), any(), any(), any()))
.thenThrow(new IllegalStateException("dm unavailable"));
when(failureRouter.route(any(), any(), any()))
.thenReturn(SyslogFailureRouter.TerminalState.PAUSED);
SysLogProcessor.BatchMetrics metrics = new SysLogProcessor.BatchMetrics();
assertThrows(IllegalStateException.class,
() -> processor.normalizePreparedMessages(messages(2), metrics));
}
@Test
void recordsRuleComputeAndDmWriteTimingsSeparately() {
when(normalProcessor.prepare(any(PreparedNormalizationInput.class), any(), any(), any()))
.thenAnswer(invocation -> {
Thread.sleep(60L);
return LogNormalProcessor.NormalizationResult.success(
Collections.singletonList(normalizedRow()));
});
when(batchWriter.write(any())).thenAnswer(invocation -> {
Thread.sleep(70L);
return new DmStandardizedBatchWriter.WriteReport<>();
});
SysLogProcessor.BatchMetrics metrics = new SysLogProcessor.BatchMetrics();
processor.normalizePreparedMessages(messages(2), metrics);
assertTrue(metrics.ruleComputeMs() >= 40L,
"规则计算耗时应覆盖并行任务等待时间");
assertTrue(metrics.dmWriteMs() >= 50L,
"达梦写入耗时应独立覆盖批写调用");
}
private List<SysLogProcessor.PreparedMessage> messages(int count) {
List<SysLogProcessor.PreparedMessage> messages = new ArrayList<>();
for (int i = 0; i < count; i++) {
ConsumerRecord<String, String> record =
new ConsumerRecord<>("raw", i % 2, i, "key-" + i, "ciphertext");
SyslogFailureRouter.MessageContext context = new SyslogFailureRouter.MessageContext(
"ciphertext", "20260811-event-" + i, 0, null, Instant.now());
messages.add(new SysLogProcessor.PreparedMessage(
record, context, "message-" + i, "20260811-event-" + i, null,
java.time.LocalDateTime.of(2026, 8, 11, 3, 55, 57, 109_000_000),
7L, 3L,
System.currentTimeMillis()));
}
return messages;
}
private java.util.Map<String, Object> normalizedRow() {
java.util.Map<String, Object> row = new java.util.HashMap<>();
row.put("id", java.util.UUID.randomUUID().toString());
row.put("rule_version", "legacy");
row.put("created_at", LocalDateTime.of(2026, 8, 11, 3, 55));
row.put("log_time", LocalDateTime.of(2026, 8, 11, 3, 55));
return row;
}
}
@@ -0,0 +1,31 @@
package com.Modules.NormalData;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.time.LocalDateTime;
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());
assertEquals(LocalDateTime.of(2026, 8, 11, 3, 55, 57, 109_000_000),
resolved.getReceivedAt());
}
@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,153 @@
package com.Modules.NormalData;
import com.common.service.DmStandardizedBatchWriter;
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.SocketException;
import java.time.Instant;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
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.verifyNoInteractions;
import static org.mockito.Mockito.when;
import org.mockito.ArgumentCaptor;
/** consumer-rule 多节点消费使用与平台端相同的 Retry/DLT 可靠性语义。 */
class SyslogFailureRouterReliabilityTest {
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() {
router = new SyslogFailureRouter(template, new CacheConfig().objectMapper());
ReflectionTestUtils.setField(router, "retry1mTopic", "raw.RETRY.1M");
ReflectionTestUtils.setField(router, "publishTimeoutMs", 1000L);
ReflectionTestUtils.setField(router, "consumerVersion", "test");
SettableListenableFuture<SendResult<String, String>> acknowledged = new SettableListenableFuture<>();
acknowledged.set(null);
when(template.send(eq("raw.RETRY.1M"), anyString(), anyString())).thenReturn(acknowledged);
SyslogFailureRouter.TerminalState state = router.route(
record, router.resolve(record), new SocketException("reset"));
assertEquals(SyslogFailureRouter.TerminalState.RETRY_PUBLISHED, state);
ArgumentCaptor<String> body = ArgumentCaptor.forClass(String.class);
verify(template).send(eq("raw.RETRY.1M"), anyString(), body.capture());
assertFalse(body.getValue().contains("\"@class\""));
}
@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);
ArgumentCaptor<String> body = 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 influxDependencyFailurePausesWithoutDltStorm() {
SyslogFailureRouter.TerminalState state = router.route(
record, router.resolve(record), new InfluxException("influx unavailable"));
assertEquals(SyslogFailureRouter.TerminalState.PAUSED, state);
verifyNoInteractions(template);
}
@Test
void isolatedPermanentDmRowIsNotMisclassifiedByNestedSqlException() {
RuntimeException failure = new DmStandardizedBatchWriter.PermanentRowWriteException(
"value too long", new java.sql.SQLException("达梦字段长度超限"));
assertEquals(SyslogFailureRouter.FailureCategory.PERMANENT_DATA, router.classify(failure));
}
@Test
void isolatedUnknownDmRowUsesFiniteRetryInsteadOfPausingPartitionForever() {
RuntimeException failure = new DmStandardizedBatchWriter.RetryableRowWriteException(
"unknown row error", new java.sql.SQLException("unknown dm row error"));
assertEquals(SyslogFailureRouter.FailureCategory.TRANSIENT, router.classify(failure));
}
@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,51 @@
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.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/** 防止多节点 consumer-rule 的监听器 ID 或显式消费组在后续合并中回退。 */
class SyslogKafkaTopologyTest {
@Test
void rawListenerUsesNormalizerIdentityAndExplicitGroup() 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-rule-raw", listener.id());
assertEquals("${spring.kafka.consumer.group-id}", listener.groupId());
assertEquals("${syslog.raw-consumer.enabled:true}", listener.autoStartup());
assertEquals("rawKafkaListenerContainerFactory", listener.containerFactory());
assertEquals("", listener.concurrency());
assertArrayEquals(new String[]{"${spring.kafka.consumer.topic:agent-syslog-topic}"}, listener.topics());
}
@Test
void retryReplayListenerIsIsolatedFromRawConsumer() throws Exception {
Method method = SysLogProcessor.class.getMethod(
"consumeRetryAndReplay", List.class,
org.springframework.kafka.support.Acknowledgment.class);
KafkaListener listener = method.getAnnotation(KafkaListener.class);
assertNotNull(listener);
assertEquals("syslog-consumer-rule-retry-replay", listener.id());
assertEquals("${syslog.reliability.consumer-group-id}", listener.groupId());
assertEquals("${syslog.reliability.consumer.enabled:true}", listener.autoStartup());
assertEquals("${syslog.reliability.listener.concurrency:1}", listener.concurrency());
assertArrayEquals(new String[]{
"${syslog.reliability.retry-1m-topic:${spring.kafka.consumer.topic}.RETRY.1M}",
"${syslog.reliability.retry-10m-topic:${spring.kafka.consumer.topic}.RETRY.10M}",
"${syslog.reliability.retry-1h-topic:${spring.kafka.consumer.topic}.RETRY.1H}",
"${syslog.reliability.replay-topic:${spring.kafka.consumer.topic}.REPLAY}"
}, listener.topics());
}
}
@@ -0,0 +1,101 @@
package com.Modules.NormalData.cache;
import com.Modules.NormalData.compiled.RuleSource;
import com.Modules.NormalData.metrics.NormalizationRuleMetrics;
import com.alicp.jetcache.Cache;
import com.alicp.jetcache.MultiLevelCache;
import com.alicp.jetcache.SimpleProxyCache;
import com.alicp.jetcache.support.Fastjson2KeyConvertor;
import com.alicp.jetcache.support.Kryo5ValueDecoder;
import com.alicp.jetcache.support.Kryo5ValueEncoder;
import com.alicp.jetcache.template.QuickConfig;
import com.alicp.jetcache.embedded.CaffeineCacheBuilder;
import com.common.entity.DeviceCollectTask;
import com.common.entity.DeviceDevice;
import com.config.NormalizationOptimizationProperties;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class JetCacheLookupProviderTest {
@Test
void resolvesLayersFromConfigAndCachesRuleSnapshot() {
Cache<Long, RuleSourceSnapshot> l1 = cache();
Cache<Long, RuleSourceSnapshot> l2 = cache();
MultiLevelCache<Long, RuleSourceSnapshot> multi =
new MultiLevelCache<Long, RuleSourceSnapshot>(l1, l2);
Cache<Long, RuleSourceSnapshot> proxy =
new SimpleProxyCache<Long, RuleSourceSnapshot>(multi);
// 复现现场根因:JetCache 2.7.8 不允许用 unwrap 取得 MultiLevelCache 自身。
assertThrows(IllegalArgumentException.class, () -> multi.unwrap(MultiLevelCache.class));
List<Cache<Long, RuleSourceSnapshot>> layers = JetCacheLookupProvider.cacheLayers(proxy);
assertEquals(2, layers.size());
assertEquals(l1, layers.get(0));
assertEquals(l2, layers.get(1));
NormalizationLookupDataLoader loader = mock(NormalizationLookupDataLoader.class);
RuleSourceSnapshot snapshot = new RuleSourceSnapshot(7L,
Collections.singletonList(new RuleSource(8L, "rule", "rule", "json",
"{\"mappers\":[]}", true, "0")), Instant.now());
when(loader.loadRules(7L)).thenReturn(snapshot);
NormalizationRuleMetrics metrics = new NormalizationRuleMetrics();
JetCacheLookupProvider provider = new JetCacheLookupProvider(
loader, new NormalizationOptimizationProperties(), metrics,
JetCacheLookupProviderTest.<Integer, DeviceDevice>cache(),
JetCacheLookupProviderTest.<Integer, DeviceCollectTask>cache(), proxy);
assertEquals(1, provider.rules(7L).getRules().size());
assertEquals(1, provider.rules(7L).getRules().size());
verify(loader, times(1)).loadRules(7L);
assertTrue(l2.get(7L) != null);
@SuppressWarnings("unchecked")
Map<String, Long> outcomes = (Map<String, Long>)
metrics.snapshot().get("rule_source_cache_get_total");
assertEquals(Long.valueOf(1L), outcomes.get("load"));
assertEquals(Long.valueOf(1L), outcomes.get("l1_hit"));
assertEquals(Long.valueOf(0L), outcomes.get("failure"));
}
@Test
void quickConfigRetainsTwoLevelCacheContract() {
QuickConfig config = JetCacheLookupProvider.quickConfig("cache-name", 300, 30);
assertEquals(com.alicp.jetcache.anno.CacheType.BOTH, config.getCacheType());
assertEquals(java.time.Duration.ofSeconds(300), config.getExpire());
assertEquals(java.time.Duration.ofSeconds(30), config.getLocalExpire());
assertEquals(Integer.valueOf(10_000), config.getLocalLimit());
assertEquals(Boolean.TRUE, config.getSyncLocal());
assertEquals(Boolean.TRUE, config.getPenetrationProtect());
assertEquals(Fastjson2KeyConvertor.INSTANCE, config.getKeyConvertor());
assertEquals(Kryo5ValueEncoder.INSTANCE, config.getValueEncoder());
assertEquals(Kryo5ValueDecoder.INSTANCE, config.getValueDecoder());
DeviceDevice value = new DeviceDevice();
value.setName("device-a");
Object decoded = config.getValueDecoder().apply(config.getValueEncoder().apply(value));
assertTrue(decoded instanceof DeviceDevice);
assertEquals("device-a", ((DeviceDevice) decoded).getName());
}
private static <K, V> Cache<K, V> cache() {
return CaffeineCacheBuilder.createCaffeineCacheBuilder()
.limit(100)
.expireAfterWrite(10, TimeUnit.MINUTES)
.buildCache();
}
}
@@ -0,0 +1,38 @@
package com.Modules.NormalData.cache;
import com.Modules.NormalData.compiled.NormalizationCacheInvalidator;
import com.Modules.NormalData.metrics.NormalizationRuleMetrics;
import org.junit.jupiter.api.Test;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class NormalizationCacheInvalidationServiceTest {
@Test
void evictsMatchingLegacyAndJetCacheKeys() {
CacheManager manager = mock(CacheManager.class);
Cache device = mock(Cache.class);
Cache task = mock(Cache.class);
Cache rules = mock(Cache.class);
when(manager.getCache("device")).thenReturn(device);
when(manager.getCache("collectTask")).thenReturn(task);
when(manager.getCache("normalizeRule")).thenReturn(rules);
NormalizationCacheInvalidator jet = mock(NormalizationCacheInvalidator.class);
NormalizationCacheInvalidationService service = new NormalizationCacheInvalidationService(
manager, jet, new NormalizationRuleMetrics());
service.invalidateDevice(7);
service.invalidateCollectTask(8);
service.invalidateRulesForDevice(9L);
verify(device).evict("device:id:7");
verify(task).evict("id:8");
verify(rules).evict("deviceId:9");
verify(jet).device(7);
verify(jet).collectTask(8);
verify(jet).rulesForDevice(9L);
}
}
@@ -0,0 +1,36 @@
package com.Modules.NormalData.cache;
import com.config.NormalizationOptimizationProperties;
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
class NormalizationLookupFacadeTest {
@Test
void routesEachCallToExactlyOneProvider() {
LegacySpringCacheLookupProvider legacy = mock(LegacySpringCacheLookupProvider.class);
JetCacheLookupProvider jet = mock(JetCacheLookupProvider.class);
NormalizationOptimizationProperties properties = new NormalizationOptimizationProperties();
NormalizationLookupFacade facade = new NormalizationLookupFacade(legacy, jet, properties);
facade.device(1);
facade.collectTask(2);
facade.rules(3L);
verify(legacy).device(1);
verify(legacy).collectTask(2);
verify(legacy).rules(3L);
verify(jet, never()).device(1);
properties.setJetcacheEnabled(true);
properties.getRuleSourceCache().setEnabled(true);
facade.device(4);
facade.collectTask(5);
facade.rules(6L);
verify(jet).device(4);
verify(jet).collectTask(5);
verify(jet).rules(6L);
verify(legacy, never()).device(4);
}
}
@@ -0,0 +1,51 @@
package com.Modules.NormalData.compiled;
import com.common.entity.DeviceCollectTask;
import com.common.entity.DeviceDevice;
import com.common.service.DeviceCollectTaskService;
import com.common.service.DeviceDeviceService;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
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.when;
class BatchEnrichmentContextTest {
@Test
void concurrentSameIdsInvokeEachLoaderOnce() throws Exception {
DeviceDeviceService deviceService = mock(DeviceDeviceService.class);
DeviceCollectTaskService taskService = mock(DeviceCollectTaskService.class);
DeviceDevice device = new DeviceDevice();
device.setName("firewall");
DeviceCollectTask task = new DeviceCollectTask();
task.setTaskName("agent-a");
when(deviceService.getByIdSafely(7)).thenReturn(device);
when(taskService.getById(3)).thenReturn(task);
BatchEnrichmentContext context = new BatchEnrichmentContext(true, taskService, deviceService);
ExecutorService executor = Executors.newFixedThreadPool(16);
try {
List<Future<?>> futures = new ArrayList<>();
for (int i = 0; i < 40; i++) {
futures.add(executor.submit(() -> {
context.device(7);
context.collectTask(3);
}));
}
for (Future<?> future : futures) future.get();
} finally {
executor.shutdownNow();
}
verify(deviceService, times(1)).getByIdSafely(7);
verify(taskService, times(1)).getById(3);
assertEquals(1, context.deviceUniqueCount());
assertEquals(1, context.collectTaskUniqueCount());
}
}
@@ -0,0 +1,32 @@
package com.Modules.NormalData.compiled;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Collections;
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.assertThrows;
class BatchRuleSnapshotTest {
@Test
void freezesRuleOrderAndDoesNotObserveSourceListMutation() {
List<RuleSource> rules = new ArrayList<>();
rules.add(new RuleSource(2L, "second", "second", "json", "{\"mappers\":[]}", true, "0"));
rules.add(new RuleSource(1L, "first", "first", "json", "{\"mappers\":[]}", true, "0"));
Map<Long, List<RuleSource>> source = new LinkedHashMap<>();
source.put(7L, rules);
BatchRuleSnapshot snapshot = new BatchRuleSnapshot(source, 123L);
rules.clear();
assertEquals(2L, snapshot.rulesFor(7L).get(0).getId());
assertEquals(1L, snapshot.rulesFor(7L).get(1).getId());
assertThrows(UnsupportedOperationException.class,
() -> snapshot.rulesFor(7L).add(new RuleSource(3L, "x", "x", "json",
"{\"mappers\":[]}", true, "0")));
assertEquals(Collections.emptyList(), snapshot.rulesFor(99L));
}
}
@@ -0,0 +1,40 @@
package com.Modules.NormalData.compiled;
import com.config.NormalizationOptimizationProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CompiledRuleCacheConcurrencyTest {
@Test
void sameFingerprintHasOneLoaderAcrossConcurrentMisses() throws Exception {
AtomicInteger compiles = new AtomicInteger();
CompiledRuleCompiler compiler = new CompiledRuleCompiler(new ObjectMapper()) {
@Override public CompiledRule compile(RuleSource source) {
compiles.incrementAndGet();
return super.compile(source);
}
};
CompiledRuleCache cache = new CompiledRuleCache(compiler, new NormalizationOptimizationProperties());
RuleSource source = new RuleSource(1L, "r", "R", "json", "{\"mappers\":[]}", true, "0");
ExecutorService executor = Executors.newFixedThreadPool(16);
try {
List<Callable<CompiledRule>> calls = new ArrayList<>();
for (int i = 0; i < 64; i++) calls.add(() -> cache.get(source, null));
List<Future<CompiledRule>> values = executor.invokeAll(calls);
for (Future<CompiledRule> value : values) value.get();
assertEquals(1, compiles.get());
} finally {
executor.shutdownNow();
}
}
}
@@ -0,0 +1,144 @@
package com.Modules.NormalData.compiled;
import com.config.NormalizationOptimizationProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CompiledRuleCacheTest {
@Test
void concurrentMissCompilesSameFingerprintOnceAndRecompilesChangedVersion() throws Exception {
CountingCompiler compiler = new CountingCompiler();
CompiledRuleCache cache = new CompiledRuleCache(compiler, properties());
RuleSource first = source("rule-a", "{\"mappers\":[]}");
ExecutorService executor = Executors.newFixedThreadPool(20);
try {
List<Callable<CompiledRule>> calls = new ArrayList<>();
for (int i = 0; i < 20; i++) calls.add(() -> cache.get(first, new RuleExecutionMetrics()));
List<Future<CompiledRule>> futures = executor.invokeAll(calls);
CompiledRule shared = futures.get(0).get(5, TimeUnit.SECONDS);
for (Future<CompiledRule> future : futures) assertEquals(shared, future.get());
assertEquals(1, compiler.compiles.get());
CompiledRule changed = cache.get(source("rule-b", "{\"mappers\":[]}"),
new RuleExecutionMetrics());
assertNotSame(shared, changed);
assertEquals(2, compiler.compiles.get());
} finally {
executor.shutdownNow();
}
}
@Test
void failedCompilationUsesShortFailureCacheNotPositiveCache() {
CountingCompiler compiler = new CountingCompiler();
CompiledRuleCache cache = new CompiledRuleCache(compiler, properties());
RuleSource bad = source("bad", "{\"mappers\":[],\"filters_params\":{\"expressions\":[{"
+ "\"expressions\":[{\"column_name\":\"x\",\"op\":\"bad\",\"value\":1}]}]}}}");
RuntimeException first = assertThrows(RuntimeException.class, () -> cache.get(bad, null));
RuntimeException cached = assertThrows(RuntimeException.class, () -> cache.get(bad, null));
assertTrue(first.getMessage().contains("未知条件操作符: bad"));
assertTrue(cached.getMessage().contains("未知条件操作符: bad"));
assertEquals(1, compiler.compiles.get());
assertEquals(0, cache.estimatedSize());
}
@Test
void concurrentBadRuleCompilesOnlyOnce() throws Exception {
CountingCompiler compiler = new CountingCompiler();
CompiledRuleCache cache = new CompiledRuleCache(compiler, properties());
RuleSource bad = source("bad-concurrent", "{\"mappers\":[{\"origin_field\":\"x\","
+ "\"dest_field\":\"y\",\"action\":{\"type\":\"unknown\",\"param\":{}}}]}");
ExecutorService executor = Executors.newFixedThreadPool(20);
try {
List<Callable<Boolean>> calls = new ArrayList<>();
for (int i = 0; i < 20; i++) {
calls.add(() -> {
try {
cache.get(bad, null);
return false;
} catch (RuntimeException expected) {
return true;
}
});
}
for (Future<Boolean> future : executor.invokeAll(calls)) assertEquals(true, future.get());
assertEquals(1, compiler.compiles.get());
} finally {
executor.shutdownNow();
}
}
@Test
void invalidatingRuleRemovesEveryHistoricalFingerprint() {
CountingCompiler compiler = new CountingCompiler();
CompiledRuleCache cache = new CompiledRuleCache(compiler, properties());
RuleSource first = source("v1", "{\"mappers\":[]}");
RuleSource second = source("v2", "{\"mappers\":[]}");
cache.get(first, null);
cache.get(second, null);
assertEquals(2, compiler.compiles.get());
cache.invalidateRule(8L);
cache.get(first, null);
cache.get(second, null);
assertEquals(4, compiler.compiles.get());
}
@Test
void diagnosticRuleTextKeepsExecutionFieldsAndRemovesEmbeddedSamples() {
String diagnostic = CompiledRuleCache.diagnosticRuleText("{"
+ "\"mappers\":[],\"filters_params\":\"{}\","
+ "\"logs\":[{\"password\":\"secret-password\"}],"
+ "\"processed_logs\":[{\"access_token\":\"secret-token\"}],"
+ "\"sample_logs\":[\"secret-cookie\"]}");
assertTrue(diagnostic.contains("\"mappers\":[]"));
assertTrue(diagnostic.contains("\"filters_params\":\"{}\""));
assertFalse(diagnostic.contains("secret-password"));
assertFalse(diagnostic.contains("secret-token"));
assertFalse(diagnostic.contains("secret-cookie"));
}
private static NormalizationOptimizationProperties properties() {
NormalizationOptimizationProperties properties = new NormalizationOptimizationProperties();
properties.getCompiledRule().setLocalLimit(100);
properties.getCompiledRule().setExpireAfterAccessMinutes(30);
properties.getCompiledRule().setFailureCacheSeconds(10);
return properties;
}
private static RuleSource source(String name, String content) {
return new RuleSource(8L, name, name, "json", content, true, "0");
}
private static final class CountingCompiler extends CompiledRuleCompiler {
private final AtomicInteger compiles = new AtomicInteger();
private CountingCompiler() {
super(new ObjectMapper());
}
@Override
public CompiledRule compile(RuleSource source) {
compiles.incrementAndGet();
return super.compile(source);
}
}
}
@@ -0,0 +1,164 @@
package com.Modules.NormalData.compiled;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import java.util.Map;
import java.util.Arrays;
import java.util.Collections;
import java.util.regex.Pattern;
import static org.junit.jupiter.api.Assertions.*;
class CompiledRuleCompilerTest {
private final CompiledRuleCompiler compiler = new CompiledRuleCompiler(new ObjectMapper());
@Test
void compilesJsonMappingFilterCompletionAndOneToManyMapper() {
RuleSource source = source(18L, "rule-a", "json", "{"
+ "\"mappers\":["
+ "{\"origin_field\":\"src_ip\",\"dest_field\":\"src_ip\",\"action\":{\"type\":\"equal\",\"param\":{}}},"
+ "{\"origin_field\":\"src_ip\",\"dest_field\":\"attacker_ip\",\"action\":{\"type\":\"equal\",\"param\":{}}},"
+ "{\"origin_field\":\"level\",\"dest_field\":\"severity\",\"action\":{\"type\":\"mapping\",\"param\":{\"mapping\":{\"high\":3}}}},"
+ "{\"origin_field\":\"event_time\",\"dest_field\":\"event_ts\",\"action\":{\"type\":\"time\",\"param\":{\"timezone\":\"UTC\"}}}],"
+ "\"filters_params\":{\"expressions\":[{\"filters_relation\":\"and\",\"expressions\":["
+ "{\"column_name\":\"src_ip\",\"op\":\"=\",\"value\":\"blocked\"}]}]},"
+ "\"complete_columns\":[{\"column_name\":\"country\",\"value\":\"CN\","
+ "\"filters_relation\":\"and\",\"conditions\":[{\"column_name\":\"src_ip\","
+ "\"op\":\"regexp\",\"value\":\"10\\\\..*\"}]}]}"
+ "}");
CompiledRule rule = compiler.compile(source);
ParsedMessage parsed = rule.parse("<14>1 host app - - - {\"src_ip\":\"10.1.2.3\",\"level\":\"high\",\"event_time\":\"2024-03-09T16:00:00Z\"}");
Map<String, Object> destination = rule.map(parsed);
assertEquals("10.1.2.3", destination.get("src_ip"));
assertEquals("10.1.2.3", destination.get("attacker_ip"));
assertEquals(3, destination.get("severity"));
assertEquals(1710000000000L, destination.get("event_ts"));
assertFalse(rule.isFiltered(destination));
rule.complete(destination);
assertEquals("CN", destination.get("country"));
assertEquals(2, rule.getMapperByOriginField().get("src_ip").size());
assertTrue(rule.toString().contains("CompiledRule{"));
assertTrue(rule.toString().contains("mapperByOriginField"));
assertTrue(rule.toString().contains("MessageParserSignature{"));
assertThrows(UnsupportedOperationException.class,
() -> rule.getMapperByOriginField().put("x", java.util.Collections.emptyList()));
}
@Test
void parsesKvSeparatorAndRegexWithCompiledParserPlans() {
CompiledRule kv = compiler.compile(source(40L, "kv", "kv", "{"
+ "\"cropper_params\":{\"head_key\":\"a\",\"head_offset\":0,\"tail_key\":\"\",\"tail_offset\":0},"
+ "\"kv_params\":{\"internal\":\"=\",\"external\":\" \",\"l_trim\":\"\",\"r_trim\":\"\"},"
+ "\"mappers\":[]}"));
assertEquals("2", kv.parse("a=1 b=2").getFields().get("b"));
CompiledRule sep = compiler.compile(source(41L, "sep", "sep", "{"
+ "\"cropper_params\":{\"head_key\":\"x\",\"head_offset\":0,\"tail_key\":\"\",\"tail_offset\":0},"
+ "\"sep\":\",\",\"mappers\":[]}"));
assertEquals("y", sep.parse("x,y,z").getFields().get("2"));
CompiledRule regex = compiler.compile(source(42L, "regex", "regex", "{"
+ "\"cropper_params\":{\"head_key\":\"src\",\"head_offset\":0,\"tail_key\":\"\",\"tail_offset\":0},"
+ "\"regexp\":\"src=(\\\\S+) dst=(\\\\S+)\",\"mappers\":[]}"));
assertEquals("10.0.0.1", regex.parse("src=10.0.0.1 dst=10.0.0.2").getFields().get("1"));
}
@Test
void evaluatesAllSupportedPredicateOperations() {
assertTrue(predicate(CompiledPredicate.Operation.EQUAL, "a", null).evaluate("a", false));
assertTrue(predicate(CompiledPredicate.Operation.NOT_EQUAL, "a", null).evaluate("b", false));
assertTrue(new CompiledPredicate("x", CompiledPredicate.Operation.IN, null,
Arrays.<Object>asList("a", "b"), null).evaluate("b", false));
assertTrue(predicate(CompiledPredicate.Operation.ZERO_LEN, true, null).evaluate("", false));
assertTrue(predicate(CompiledPredicate.Operation.CONTAIN, "bc", null).evaluate("abcd", false));
assertTrue(predicate(CompiledPredicate.Operation.REGEXP, "a.*", Pattern.compile("a.*"))
.evaluate("abcd", false));
assertFalse(predicate(CompiledPredicate.Operation.NOT_EQUAL, null, null).evaluate("x", false));
assertTrue(predicate(CompiledPredicate.Operation.NOT_EQUAL, null, null).evaluate("x", true));
}
@Test
void rejectsInvalidRegexAndUnknownOperation() {
RuleSource invalidRegex = source(20L, "bad-regex", "regex",
"{\"regexp\":\"([\",\"mappers\":[]}");
assertThrows(CompiledRuleCompiler.RuleCompilationException.class,
() -> compiler.compile(invalidRegex));
RuleSource invalidOperation = source(21L, "bad-op", "json",
"{\"mappers\":[],\"filters_params\":{\"expressions\":[{"
+ "\"expressions\":[{\"column_name\":\"x\",\"op\":\"startsWith\",\"value\":\"a\"}]}]}}}");
assertThrows(CompiledRuleCompiler.RuleCompilationException.class,
() -> compiler.compile(invalidOperation));
}
@Test
void acceptsLegacyStringOffsetsAndTreatsNonObjectFiltersAsEmpty() {
RuleSource legacyShape = source(1989234824495550465L, "legacy-shape", "json", "{"
+ "\"cropper_params\":{\"head_key\":\"{\",\"head_offset\":\"0\","
+ "\"tail_key\":\"\",\"tail_offset\":\"-1\"},"
+ "\"mappers\":[],\"filters_params\":[],\"complete_columns\":{}}");
CompiledRule compiled = compiler.compile(legacyShape);
assertEquals(0, compiled.getCropper().getHeadOffset());
assertEquals(-1, compiled.getCropper().getTailOffset());
assertTrue(compiled.getFilters().isEmpty());
assertTrue(compiled.getCompletions().isEmpty());
}
@Test
void decodesStringMappersButIgnoresStringFiltersAndCompletionsLikeLegacy() {
RuleSource legacyShape = source(1996235894300086273L, "embedded-json", "json", "{"
+ "\"mappers\":\"[{\\\"origin_field\\\":\\\"src\\\","
+ "\\\"dest_field\\\":\\\"dest\\\","
+ "\\\"action\\\":{\\\"type\\\":\\\"equal\\\",\\\"param\\\":{}}}]\","
+ "\"filters_params\":\"{\\\"expressions\\\":[{\\\"expressions\\\":[{"
+ "\\\"column_name\\\":\\\"dest\\\",\\\"op\\\":\\\"=\\\","
+ "\\\"value\\\":\\\"blocked\\\"}]}]}\","
+ "\"complete_columns\":\"[{\\\"column_name\\\":\\\"country\\\","
+ "\\\"value\\\":\\\"CN\\\",\\\"conditions\\\":[]}]\"}");
CompiledRule compiled = compiler.compile(legacyShape);
assertEquals(1, compiled.getMapperByOriginField().get("src").size());
assertTrue(compiled.getFilters().isEmpty());
assertTrue(compiled.getCompletions().isEmpty());
}
@Test
void compilesBlankLegacyActionAsNullWithoutUsingOuterType() {
RuleSource legacyShape = source(2075231829946646529L, "blank-action", "json", "{"
+ "\"mappers\":[{\"origin_field\":\"rule_name_list[0]\","
+ "\"dest_field\":\"origin_rule_name\",\"type\":\"equal\","
+ "\"action\":{\"type\":\"\",\"param\":{}}}]}");
CompiledRule compiled = compiler.compile(legacyShape);
Map<String, Object> destination = compiled.map(
new ParsedMessage(Collections.<String, Object>singletonMap("rule_name_list[0]", "x")));
assertTrue(destination.containsKey("origin_rule_name"));
assertNull(destination.get("origin_rule_name"));
assertEquals(CompiledMapper.Action.LEGACY_NULL,
compiled.getMapperByOriginField().get("rule_name_list[0]").get(0).getAction());
}
@Test
void fingerprintUsesExecutionContentButNotUpdateTime() {
RuleSource first = source(22L, "a", "json", "{\"mappers\":[]}");
RuleSource renamed = source(22L, "b", "json", "{\"mappers\":[]}");
assertNotEquals(compiler.fingerprint(first), compiler.fingerprint(renamed));
assertTrue(compiler.fingerprint(first).startsWith("22:"));
}
private static RuleSource source(long id, String name, String type, String content) {
return new RuleSource(id, name, name, type, content, true, "0");
}
private static CompiledPredicate predicate(CompiledPredicate.Operation operation,
Object value, Pattern pattern) {
return new CompiledPredicate("x", operation, value, Collections.emptyList(), pattern);
}
}
@@ -0,0 +1,64 @@
package com.Modules.NormalData.compiled;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** 使用达梦管理工具导出的规则做离线全量编译验证;不输出规则正文。 */
class CompiledRuleExportValidationTest {
@Test
void compilesExportedRulesWhenPathProvided() throws Exception {
String exportPath = System.getProperty("normalization.rule-export");
if (exportPath == null || exportPath.trim().isEmpty()) return;
ObjectMapper objectMapper = new ObjectMapper();
JsonNode exportRoot = objectMapper.readTree(
new String(Files.readAllBytes(Paths.get(exportPath)), java.nio.charset.StandardCharsets.UTF_8));
JsonNode rows = exportedRows(exportRoot);
assertTrue(rows != null && rows.isArray(), "规则导出文件必须包含一个数组结果集");
CompiledRuleCompiler compiler = new CompiledRuleCompiler(objectMapper);
List<String> failures = new ArrayList<>();
for (JsonNode row : rows) {
long id = row.path("id").asLong();
String name = row.path("name").asText();
RuleSource source = new RuleSource(id, name, row.path("display_name").asText(),
row.path("data_type").asText(), row.path("rule_content").asText(), true, "0");
try {
compiler.compile(source);
} catch (RuntimeException failure) {
failures.add("ruleId=" + id + ", name=" + name + ", reason=" + safeReason(failure));
}
}
assertTrue(failures.isEmpty(), () -> "全量规则编译失败:\n" + String.join("\n", failures));
}
private static JsonNode exportedRows(JsonNode root) {
if (root == null) return null;
if (root.isArray()) return root;
if (!root.isObject()) return null;
Iterator<Map.Entry<String, JsonNode>> fields = root.fields();
while (fields.hasNext()) {
JsonNode candidate = fields.next().getValue();
if (candidate.isArray()) return candidate;
}
return null;
}
private static String safeReason(RuntimeException failure) {
String reason = failure.getMessage();
if (reason == null || reason.trim().isEmpty()) return failure.getClass().getSimpleName();
return reason.replace('\r', ' ').replace('\n', ' ').trim();
}
}
@@ -0,0 +1,24 @@
package com.Modules.NormalData.compiled;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
class CompiledRuleFingerprintTest {
private final CompiledRuleCompiler compiler = new CompiledRuleCompiler(new ObjectMapper());
@Test
void identityIsStableSha256OfCanonicalExecutionFields() {
RuleSource first = new RuleSource(4L, "r", "display-a", "json",
"{\"mappers\":[]}", true, "0");
RuleSource sameExecution = new RuleSource(4L, "r", "display-b", "json",
"{\"mappers\":[]}", true, "0");
RuleSource changedValidity = new RuleSource(4L, "r", "display-b", "json",
"{\"mappers\":[]}", false, "0");
assertEquals(compiler.fingerprint(first), compiler.fingerprint(sameExecution));
assertNotEquals(compiler.fingerprint(first), compiler.fingerprint(changedValidity));
assertEquals(64, first.getFingerprint().length());
}
}
@@ -0,0 +1,41 @@
package com.Modules.NormalData.compiled;
import com.alicp.jetcache.support.Kryo5ValueDecoder;
import com.alicp.jetcache.support.Kryo5ValueEncoder;
import com.common.entity.DeviceCollectTask;
import com.common.entity.DeviceDevice;
import com.Modules.NormalData.cache.RuleSourceSnapshot;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import java.time.Instant;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class JetCacheSerializationTest {
@Test
void serializesAllThreeRemoteCacheValueTypes() {
RuleSource rule = new RuleSource(1L, "rule", "Rule", "json",
"{\"mappers\":[]}", true, "0");
Object decodedRules = roundTrip(new RuleSourceSnapshot(7L, Arrays.asList(rule), Instant.EPOCH));
assertTrue(decodedRules instanceof RuleSourceSnapshot);
RuleSourceSnapshot snapshot = (RuleSourceSnapshot) decodedRules;
assertEquals(7L, snapshot.getDeviceId());
assertEquals("{\"mappers\":[]}", snapshot.getRules().get(0).getRuleContent());
assertEquals(rule.getFingerprint(), snapshot.getRules().get(0).getFingerprint());
DeviceDevice device = new DeviceDevice();
device.setName("device-a");
assertEquals("device-a", ((DeviceDevice) roundTrip(device)).getName());
DeviceCollectTask task = new DeviceCollectTask();
task.setTaskName("collector-a");
assertEquals("collector-a", ((DeviceCollectTask) roundTrip(task)).getTaskName());
}
private static Object roundTrip(Object value) {
return Kryo5ValueDecoder.INSTANCE.apply(Kryo5ValueEncoder.INSTANCE.apply(value));
}
}
@@ -0,0 +1,33 @@
package com.Modules.NormalData.metrics;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class NormalizationRuleMetricsTest {
@SuppressWarnings("unchecked")
@Test
void exposesEveryRequiredFixedDimension() {
NormalizationRuleMetrics metrics = new NormalizationRuleMetrics();
metrics.compiledGet("hit");
metrics.compiled("success", 2_000_000L);
metrics.shadow("match");
metrics.ruleSource("load");
metrics.enrichment("device", "hit");
metrics.invalidation("after_commit");
metrics.messageParse("miss");
metrics.ruleJsonParse();
metrics.compiledCacheGauge(3, 1);
Map<String, Object> snapshot = metrics.snapshot();
assertEquals(1L, ((Map<String, Long>) snapshot.get("compiled_rule_cache_get_total")).get("hit"));
assertEquals(3L, snapshot.get("compiled_rule_cache_size"));
assertTrue(snapshot.containsKey("rule_source_cache_get_total"));
assertTrue(snapshot.containsKey("enrichment_cache_get_total"));
assertTrue(snapshot.containsKey("rule_invalidation_total"));
assertEquals(1L, snapshot.get("rule_json_parse_total"));
}
}
@@ -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,78 @@
package com.common.service;
import com.common.entity.SyslogNonNormalMessage;
import org.junit.jupiter.api.Test;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
class DirectStatsAccumulatorTest {
@Test
void oneThousandFactsCollapseToDeviceAndRuleDimensionsInStableOrder() {
List<Map<String, Object>> rows = new ArrayList<>();
LocalDateTime time = LocalDateTime.of(2026, 8, 15, 4, 30);
for (int i = 0; i < 1000; i++) {
Map<String, Object> row = new HashMap<>();
row.put("created_at", time);
row.put("log_time", time.plusSeconds(i));
row.put("device_id", i % 2 == 0 ? 8L : 7L);
row.put("normalize_rule_id", i % 3 == 0 ? 12L : 11L);
rows.add(row);
}
DirectStatsAccumulator result = new DirectStatsAccumulator().addStandardizedRows(rows);
assertEquals(1000, result.getInputRows());
assertEquals(2, result.getDevices().size());
assertEquals(2, result.getRules().size());
List<Long> devices = new ArrayList<>();
for (DirectStatsAccumulator.DailyKey key : result.getDevices().keySet()) {
devices.add(key.getDimensionId());
}
assertEquals(java.util.Arrays.asList(7L, 8L), devices);
assertEquals(500L, result.getDevices().get(
new DirectStatsAccumulator.DailyKey(LocalDate.of(2026, 8, 15), 7L)).getParseDelta());
assertEquals(time.plusSeconds(999), result.getRules().get(
new DirectStatsAccumulator.DailyKey(LocalDate.of(2026, 8, 15), 12L)).getMaxLogTime());
}
@Test
void nonNormalUpdateMovesContributionBetweenDimensions() {
SyslogNonNormalMessage oldValue = new SyslogNonNormalMessage();
oldValue.setCreatedAt(OffsetDateTime.of(2026, 8, 14, 23, 0, 0, 0, ZoneOffset.UTC));
oldValue.setDeviceId(7);
SyslogNonNormalMessage patch = new SyslogNonNormalMessage();
patch.setCreatedAt(OffsetDateTime.of(2026, 8, 15, 1, 0, 0, 0, ZoneOffset.UTC));
patch.setDeviceId(8);
DirectStatsAccumulator result = new DirectStatsAccumulator().addNonNormalUpdate(oldValue, patch);
assertEquals(-1L, result.getDevices().get(new DirectStatsAccumulator.DailyKey(
DirectStatsAccumulator.dateValue(oldValue.getCreatedAt()), 7L)).getNonLogDelta());
assertEquals(1L, result.getDevices().get(new DirectStatsAccumulator.DailyKey(
DirectStatsAccumulator.dateValue(patch.getCreatedAt()), 8L)).getNonLogDelta());
}
@Test
void offsetTimestampUsesConfiguredBusinessZoneInsteadOfJvmDefault() {
SyslogNonNormalMessage message = new SyslogNonNormalMessage();
message.setCreatedAt(OffsetDateTime.of(2026, 8, 14, 18, 30, 0, 0, ZoneOffset.UTC));
message.setDeviceId(9);
DirectStatsAccumulator result = new DirectStatsAccumulator(ZoneId.of("Asia/Shanghai"))
.addNonNormalInsert(message);
assertEquals(1L, result.getDevices().get(new DirectStatsAccumulator.DailyKey(
LocalDate.of(2026, 8, 15), 9L)).getNonLogDelta());
}
}
@@ -0,0 +1,63 @@
package com.common.service;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.SimpleTransactionStatus;
import java.sql.Date;
import static org.junit.jupiter.api.Assertions.assertEquals;
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.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class DirectStatsDayRolloverServiceTest {
@Test
void directModeClearsOnlyStaleRowsUsingExplicitBusinessDateOncePerDay() {
JdbcTemplate jdbc = mock(JdbcTemplate.class);
DirectStatsDayRolloverService service = new DirectStatsDayRolloverService(
jdbc, transactionManager(), new StatsWriteModeProvider("DIRECT"));
ReflectionTestUtils.setField(service, "businessZone", "Asia/Shanghai");
ReflectionTestUtils.setField(service, "transactionTimeoutSeconds", 45);
service.scheduledRollover();
service.scheduledRollover();
ArgumentCaptor<String> sql = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<Object> firstDate = ArgumentCaptor.forClass(Object.class);
ArgumentCaptor<Object> secondDate = ArgumentCaptor.forClass(Object.class);
verify(jdbc).update(sql.capture(), firstDate.capture(), secondDate.capture());
assertTrue(sql.getValue().contains("today_stats_date<?"));
assertTrue(firstDate.getValue() instanceof Date);
assertEquals(firstDate.getValue(), secondDate.getValue());
assertEquals(0, service.status().get("last_updated_devices"));
}
@Test
void legacyModeNeverTouchesNewDateColumn() {
JdbcTemplate jdbc = mock(JdbcTemplate.class);
DirectStatsDayRolloverService service = new DirectStatsDayRolloverService(
jdbc, transactionManager(), new StatsWriteModeProvider("LEGACY_CHANGE_LOG"));
service.scheduledRollover();
verify(jdbc, never()).update(anyString(), any(), any());
}
private PlatformTransactionManager transactionManager() {
PlatformTransactionManager manager = mock(PlatformTransactionManager.class);
TransactionStatus status = new SimpleTransactionStatus();
when(manager.getTransaction(any(TransactionDefinition.class))).thenReturn(status);
return manager;
}
}
@@ -0,0 +1,122 @@
package com.common.service;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Answers;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.time.LocalDateTime;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
class DirectStatsProjectionWriterTest {
@AfterEach
void cleanupTransactionMarker() {
TransactionSynchronizationManager.clear();
}
@Test
void directModeWritesOnlyDimensionBatchesAndMaterializesAffectedRows() {
JdbcTemplate jdbc = mock(JdbcTemplate.class);
DirectStatsProjectionWriter writer = writer(jdbc, "DIRECT", true);
TransactionSynchronizationManager.setActualTransactionActive(true);
List<Map<String, Object>> rows = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Map<String, Object> row = new HashMap<>();
row.put("created_at", LocalDateTime.now());
row.put("log_time", LocalDateTime.now().plusSeconds(i));
row.put("device_id", 7L);
row.put("normalize_rule_id", i % 2 == 0 ? 11L : 12L);
rows.add(row);
}
writer.writeStandardizedRows(rows);
ArgumentCaptor<String> sql = ArgumentCaptor.forClass(String.class);
verify(jdbc, times(4)).batchUpdate(sql.capture(), anyList());
String combined = String.join("\n", sql.getAllValues());
assertTrue(combined.contains("MERGE INTO device_daily_stats"));
assertTrue(combined.contains("MERGE INTO normalize_rule_daily_stats"));
assertTrue(combined.contains("UPDATE device_device"));
assertTrue(combined.contains("UPDATE dm_normalize_rule"));
assertTrue(((Number) writer.status().get("input_rows")).longValue() == 100L);
assertTrue(((Number) writer.status().get("device_groups")).longValue() == 1L);
assertTrue(((Number) writer.status().get("rule_groups")).longValue() == 2L);
}
@Test
void legacyModeDoesNotTouchDirectTables() {
JdbcTemplate jdbc = mock(JdbcTemplate.class);
DirectStatsProjectionWriter writer = writer(jdbc, "LEGACY_CHANGE_LOG", true);
writer.writeStandardizedRow(new HashMap<>());
verifyNoInteractions(jdbc);
}
@Test
void nonNormalUpdateRecomputesOnlyAffectedFactGroupsExactly() {
AtomicInteger exactQueries = new AtomicInteger();
JdbcTemplate jdbc = mock(JdbcTemplate.class, invocation -> {
if ("queryForMap".equals(invocation.getMethod().getName())) {
exactQueries.incrementAndGet();
Map<String, Object> counts = new HashMap<>();
counts.put("parse_count", 12L);
counts.put("non_log_count", 3L);
return counts;
}
return Answers.RETURNS_DEFAULTS.answer(invocation);
});
DirectStatsProjectionWriter writer = writer(jdbc, "DIRECT", true);
TransactionSynchronizationManager.setActualTransactionActive(true);
LocalDate today = LocalDate.now(ZoneId.of("Asia/Shanghai"));
com.common.entity.SyslogNonNormalMessage oldValue =
new com.common.entity.SyslogNonNormalMessage();
oldValue.setCreatedAt(today.minusDays(1).atStartOfDay(ZoneId.of("Asia/Shanghai")).toOffsetDateTime());
oldValue.setDeviceId(7);
com.common.entity.SyslogNonNormalMessage patch =
new com.common.entity.SyslogNonNormalMessage();
patch.setCreatedAt(today.atStartOfDay(ZoneId.of("Asia/Shanghai")).toOffsetDateTime());
patch.setDeviceId(8);
writer.writeNonNormalUpdate(oldValue, patch);
assertTrue(exactQueries.get() == 2);
ArgumentCaptor<String> sql = ArgumentCaptor.forClass(String.class);
verify(jdbc, times(2)).batchUpdate(sql.capture(), anyList());
assertTrue(sql.getAllValues().get(0).contains("parse_count=s.parse_count"));
assertTrue(sql.getAllValues().get(1).contains("UPDATE device_device"));
}
private DirectStatsProjectionWriter writer(JdbcTemplate jdbc, String mode, boolean readEnabled) {
StatsWriteModeProvider modeProvider = new StatsWriteModeProvider(mode);
DirectStatsProjectionWriter writer = new DirectStatsProjectionWriter(
jdbc, modeProvider, new DirectStatsWriteMetrics());
ReflectionTestUtils.setField(writer, "readEnabled", readEnabled);
ReflectionTestUtils.setField(writer, "directDeviceTable", "device_daily_stats");
ReflectionTestUtils.setField(writer, "directRuleTable", "normalize_rule_daily_stats");
ReflectionTestUtils.setField(writer, "shadowDeviceTable", "device_daily_stats_direct_shadow");
ReflectionTestUtils.setField(writer, "shadowRuleTable", "normalize_rule_daily_stats_direct_shadow");
ReflectionTestUtils.setField(writer, "controlTable", "stats_direct_control");
ReflectionTestUtils.setField(writer, "todayStatsDateEnabled", true);
ReflectionTestUtils.setField(writer, "businessZone", "Asia/Shanghai");
return writer;
}
}
@@ -0,0 +1,76 @@
package com.common.service;
import com.Modules.NormalData.cache.NormalizeRuleChangedEvent;
import com.Modules.NormalData.compiled.CompiledRuleCompiler;
import com.common.entity.DmNormalizeRule;
import com.common.mapper.DmNormalizeRuleMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
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 DmNormalizeRuleServiceUpdateTest {
private DmNormalizeRuleMapper mapper;
private ApplicationEventPublisher publisher;
private DmNormalizeRuleService service;
@BeforeEach
void setUp() {
mapper = mock(DmNormalizeRuleMapper.class);
publisher = mock(ApplicationEventPublisher.class);
service = new DmNormalizeRuleService();
ReflectionTestUtils.setField(service, "dmNormalizeRuleMapper", mapper);
ReflectionTestUtils.setField(service, "compiledRuleCompiler",
new CompiledRuleCompiler(new ObjectMapper()));
ReflectionTestUtils.setField(service, "eventPublisher", publisher);
}
@Test
void validatesMergedPartialUpdateAndPublishesOnlyAfterChangedRow() {
DmNormalizeRule current = validRule();
when(mapper.selectById(11L)).thenReturn(current);
when(mapper.selectDeviceIdsByRuleId(11L)).thenReturn(Arrays.asList(2L, 3L));
when(mapper.updateById(any(DmNormalizeRule.class))).thenReturn(1);
DmNormalizeRule patch = new DmNormalizeRule();
patch.setId(11L);
patch.setDisplayName("renamed");
assertTrue(service.updateById(patch));
verify(publisher).publishEvent(any(NormalizeRuleChangedEvent.class));
}
@Test
void zeroRowUpdateDoesNotPublishInvalidationEvent() {
when(mapper.selectById(11L)).thenReturn(validRule());
when(mapper.selectDeviceIdsByRuleId(11L)).thenReturn(Arrays.asList(2L));
when(mapper.updateById(any(DmNormalizeRule.class))).thenReturn(0);
DmNormalizeRule patch = new DmNormalizeRule();
patch.setId(11L);
assertFalse(service.updateById(patch));
verify(publisher, never()).publishEvent(any());
}
private static DmNormalizeRule validRule() {
DmNormalizeRule rule = new DmNormalizeRule();
rule.setId(11L);
rule.setName("rule");
rule.setDisplayName("Rule");
rule.setDataType("json");
rule.setRuleContent("{\"mappers\":[]}");
rule.setIsRunning(true);
rule.setDelFlag("0");
return rule;
}
}
@@ -0,0 +1,126 @@
package com.common.service;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.concurrent.Executor;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/** 验证坏行隔离和系统故障停止规则,防止一个失败批次永久卡住后续日志。 */
class DmStandardizedBatchWriterTest {
private SyslogNormalDataService normalDataService;
private DmStandardizedBatchExecutor batchExecutor;
private DmStandardizedBatchWriter writer;
@BeforeEach
void setUp() {
normalDataService = mock(SyslogNormalDataService.class);
batchExecutor = mock(DmStandardizedBatchExecutor.class);
// 单元测试使用当前线程执行 group task,避免测试线程池掩盖真实异常。
Executor directExecutor = Runnable::run;
writer = new DmStandardizedBatchWriter(normalDataService, batchExecutor, directExecutor);
ReflectionTestUtils.setField(writer, "mode", "BATCH");
ReflectionTestUtils.setField(writer, "batchSize", 100);
ReflectionTestUtils.setField(writer, "transactionTimeoutSeconds", 45);
ReflectionTestUtils.setField(writer, "kafkaBatchDeadlineMs", 90000L);
ReflectionTestUtils.setField(writer, "maxSplitDepth", 10);
ReflectionTestUtils.setField(writer, "maxFallbackOperations", 100);
ReflectionTestUtils.setField(writer, "circuitBreakerFailureThreshold", 3);
ReflectionTestUtils.setField(writer, "circuitBreakerBackoffMs", 30000L);
when(normalDataService.isAlarmData(any())).thenReturn(false);
}
@Test
void oneBadRowIsIsolatedWithoutDowngradingEveryGoodRowToSingleInsert() {
List<DmStandardizedBatchWriter.WriteRequest<String>> requests = requests(100, 73);
// 只要当前批次包含坏行,模拟达梦在 flushStatements() 时回滚该小事务。
doAnswer(invocation -> {
List<Map<String, Object>> rows = invocation.getArgument(1);
if (containsBadRow(rows)) {
throw new DataIntegrityViolationException("value too long");
}
return null;
}).when(batchExecutor).execute(anyBoolean(), anyList(), anyInt());
// 二分最终缩小到坏行时,单条兼容路径仍会稳定复现相同数据错误。
doAnswer(invocation -> {
Map<String, Object> row = invocation.getArgument(0);
if (Boolean.TRUE.equals(row.get("bad"))) {
throw new DataIntegrityViolationException("value too long");
}
return 1;
}).when(normalDataService).insertDynamic(any());
DmStandardizedBatchWriter.WriteReport<String> report = writer.write(requests);
assertEquals(99, report.getSuccessfulRows());
assertEquals(1, report.getFailures().size());
assertTrue(report.getFailures().get("source-73")
instanceof DmStandardizedBatchWriter.PermanentRowWriteException);
// 正常行始终以批次成功,不会因为一条坏数据全部退化为逐条 INSERT。
verify(normalDataService, times(1)).insertDynamic(requests.get(73).getData());
verify(normalDataService, times(1)).insertDynamic(any());
}
@Test
void databaseTimeoutStopsImmediatelyAndNeverFallsBackToSingleRequests() {
doThrow(new QueryTimeoutException("dm timeout"))
.when(batchExecutor).execute(anyBoolean(), anyList(), anyInt());
assertThrows(DmStandardizedBatchWriter.SystemicWriteException.class,
() -> writer.write(requests(100, -1)));
// 超时影响范围不是某一行。逐条重试会把一次超时放大成 100 次,因此必须完全禁止。
verify(normalDataService, never()).insertDynamic(any());
}
private List<DmStandardizedBatchWriter.WriteRequest<String>> requests(int count, int badIndex) {
List<DmStandardizedBatchWriter.WriteRequest<String>> requests = new ArrayList<>();
for (int i = 0; i < count; i++) {
Map<String, Object> row = new HashMap<>();
row.put("id", "row-" + i);
row.put("rule_version", "legacy");
row.put("created_at", LocalDateTime.of(2026, 8, 11, 3, 55));
row.put("log_time", LocalDateTime.of(2026, 8, 11, 3, 55));
// 所有行都保留相同列签名;坏行只改变值,确保它真的混在同一个 JDBC batch 中。
row.put("bad", i == badIndex);
requests.add(new DmStandardizedBatchWriter.WriteRequest<>("source-" + i, row));
}
return requests;
}
private boolean containsBadRow(List<Map<String, Object>> rows) {
for (Map<String, Object> row : rows) {
if (Boolean.TRUE.equals(row.get("bad"))) {
return true;
}
}
return false;
}
}
@@ -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,44 @@
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;
class IngressStatsProjectionWriterTest {
@Test
void aggregatesBatchByDeviceDateAndCollectorUsingStableMinMax() {
LocalDateTime t1 = LocalDateTime.of(2026, 8, 13, 10, 0);
LocalDateTime t2 = t1.plusMinutes(2);
LocalDateTime previousDay = t1.minusDays(1);
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", 7L, 3L, previousDay),
new IngressStatsEvent("d", 8L, 4L, t1)));
assertEquals(3, result.devices.size());
assertEquals(2, result.collects.size());
assertEquals(previousDay, 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()));
}
}
@@ -0,0 +1,66 @@
package com.common.service;
import com.common.entity.StatsChangeRecord;
import com.common.mapper.StatsChangeLogMapper;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
class StatsChangeCaptureServiceTest {
@Test
void factChangeAndPendingQueueAreWrittenTogether() {
StatsChangeLogMapper mapper = mock(StatsChangeLogMapper.class);
when(mapper.insert(any())).thenReturn(1);
when(mapper.insertPending(any())).thenReturn(1);
StatsChangeCaptureService service = new StatsChangeCaptureService(
mapper, new StatsWriteModeProvider("LEGACY_CHANGE_LOG"));
ReflectionTestUtils.setField(service, "enabled", true);
ReflectionTestUtils.setField(service, "deviceReceiveCaptureEnabled", false);
Map<String, Object> row = new HashMap<>();
row.put("id", "result-1");
row.put("created_at", LocalDateTime.of(2026, 8, 13, 14, 0));
row.put("log_time", LocalDateTime.of(2026, 8, 13, 13, 59));
row.put("device_id", 7L);
row.put("normalize_rule_id", 11L);
service.captureStandardizedInsert(row, false);
org.mockito.InOrder order = inOrder(mapper);
order.verify(mapper).insert(any(StatsChangeRecord.class));
order.verify(mapper).insertPending(any(StatsChangeRecord.class));
}
@Test
void disabledCaptureDoesNotRequireNewDatabaseObjects() {
StatsChangeLogMapper mapper = mock(StatsChangeLogMapper.class);
StatsChangeCaptureService service = new StatsChangeCaptureService(
mapper, new StatsWriteModeProvider("DIRECT"));
ReflectionTestUtils.setField(service, "enabled", false);
service.captureStandardizedInsert(new HashMap<>(), false);
verifyNoInteractions(mapper);
}
@Test
void directModeNeverWritesLegacyChangeTablesEvenIfOldFlagWasLeftOn() {
StatsChangeLogMapper mapper = mock(StatsChangeLogMapper.class);
StatsChangeCaptureService service = new StatsChangeCaptureService(
mapper, new StatsWriteModeProvider("DIRECT"));
ReflectionTestUtils.setField(service, "enabled", true);
service.captureStandardizedInsert(new HashMap<>(), false);
verifyNoInteractions(mapper);
}
}
@@ -0,0 +1,110 @@
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.time.LocalDateTime;
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);
}
@Test
void dmUniqueViolationWithExistingNormalResultIsIdempotentSuccess() {
Map<String, Object> data = idempotencyData(false);
when(normalDataMapper.insertDynamic(anyMap())).thenThrow(dmUniqueViolation());
when(normalDataMapper.countByLogTimeAndId(
(LocalDateTime) data.get("log_time"), (String) data.get("id")))
.thenReturn(1);
assertEquals(0, service.insertDynamic(data));
verify(normalDataMapper).countByLogTimeAndId(
(LocalDateTime) data.get("log_time"), (String) data.get("id"));
verifyNoInteractions(normalAlarmMapper);
}
@Test
void dmUniqueViolationWithExistingAlarmResultIsIdempotentSuccess() {
Map<String, Object> data = idempotencyData(true);
when(normalAlarmMapper.insertDynamic(anyMap())).thenThrow(dmUniqueViolation());
when(normalAlarmMapper.countByLogTimeAndId(
(LocalDateTime) data.get("log_time"), (String) data.get("id")))
.thenReturn(1);
assertEquals(0, service.insertDynamic(data));
verify(normalAlarmMapper).countByLogTimeAndId(
(LocalDateTime) data.get("log_time"), (String) data.get("id"));
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);
}
@Test
void idempotentWriteWithoutStablePartitionTimeIsRejected() {
Map<String, Object> data = idempotencyData(false);
data.remove("log_time");
assertThrows(IllegalArgumentException.class, () -> service.insertDynamic(data));
verifyNoInteractions(normalDataMapper, 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");
data.put("id", "4579cbe6-54c2-38df-a0d0-7f2b811c2031");
LocalDateTime firstReceiveTime = LocalDateTime.of(2026, 8, 11, 3, 55, 57, 109_000_000);
data.put("created_at", firstReceiveTime);
data.put("log_time", firstReceiveTime);
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,43 @@
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 DirectStatsConfigurationVerifierTest {
@Test
void acceptsLegacyShadowAndDirectModes() {
assertDoesNotThrow(() -> verifier("LEGACY_CHANGE_LOG", true, true, false,
"device_daily_stats_direct_shadow", "normalize_rule_daily_stats_direct_shadow").validate());
assertDoesNotThrow(() -> verifier("DIRECT_SHADOW", true, true, false,
"device_daily_stats_direct_shadow", "normalize_rule_daily_stats_direct_shadow").validate());
assertDoesNotThrow(() -> verifier("DIRECT", false, true, false,
"device_daily_stats_direct_shadow", "normalize_rule_daily_stats_direct_shadow").validate());
}
@Test
void rejectsDoubleCountingAndUnconfirmedFactOnlyModes() {
assertThrows(IllegalStateException.class, () -> verifier("DIRECT", true, true, false,
"device_daily_stats_direct_shadow", "normalize_rule_daily_stats_direct_shadow").validate());
assertThrows(IllegalStateException.class, () -> verifier("DIRECT_SHADOW", true, true, false,
"device_daily_stats", "normalize_rule_daily_stats_direct_shadow").validate());
assertThrows(IllegalStateException.class, () -> verifier("FACT_ONLY_REBUILD_REQUIRED", false, false, false,
"device_daily_stats_direct_shadow", "normalize_rule_daily_stats_direct_shadow").validate());
assertThrows(IllegalStateException.class, () -> new DirectStatsConfigurationVerifier(
"DIRECT", false, true, false, true,
"device_daily_stats", "normalize_rule_daily_stats",
"device_daily_stats_direct_shadow", "normalize_rule_daily_stats_direct_shadow",
"Not/A-Time-Zone").validate());
}
private DirectStatsConfigurationVerifier verifier(String mode, boolean capture, boolean read,
boolean factOnlyConfirmed,
String shadowDevice, String shadowRule) {
return new DirectStatsConfigurationVerifier(mode, capture, read, factOnlyConfirmed,
true,
"device_daily_stats", "normalize_rule_daily_stats", shadowDevice, shadowRule,
"Asia/Shanghai");
}
}
@@ -0,0 +1,21 @@
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 IngressStatsConfigurationVerifierTest {
@Test
void acceptsDisabledAndShadowAndCutoverModes() {
assertDoesNotThrow(() -> new IngressStatsConfigurationVerifier(false, false).validate());
assertDoesNotThrow(() -> new IngressStatsConfigurationVerifier(true, false).validate());
assertDoesNotThrow(() -> new IngressStatsConfigurationVerifier(true, true).validate());
}
@Test
void rejectsBusinessWriteWithoutIngressProjection() {
assertThrows(IllegalStateException.class,
() -> new IngressStatsConfigurationVerifier(false, true).validate());
}
}
@@ -0,0 +1,61 @@
package com.config;
import org.junit.jupiter.api.Test;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.StandardEnvironment;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
class JetCacheRedisEnvironmentPostProcessorTest {
@Test
void derivesEncodedJetCacheUriFromExistingSpringRedisProperties() {
StandardEnvironment environment = new StandardEnvironment();
Map<String, Object> properties = new HashMap<>();
properties.put("spring.redis.host", "redis.internal");
properties.put("spring.redis.port", "6380");
properties.put("spring.redis.database", "4");
properties.put("spring.redis.password", "p@ss:word");
environment.getPropertySources().addFirst(new MapPropertySource("test", properties));
new JetCacheRedisEnvironmentPostProcessor().postProcessEnvironment(environment, null);
assertEquals("redis://:p%40ss:word@redis.internal:6380/4",
environment.getProperty("jetcache.remote.default.uri.0"));
}
@Test
void omitsAuthenticationForEmptyPassword() {
StandardEnvironment environment = new StandardEnvironment();
Map<String, Object> properties = new HashMap<>();
properties.put("spring.redis.host", "localhost");
properties.put("spring.redis.password", "");
environment.getPropertySources().addFirst(new MapPropertySource("test", properties));
new JetCacheRedisEnvironmentPostProcessor().postProcessEnvironment(environment, null);
assertEquals("redis://localhost:6379/0",
environment.getProperty("jetcache.remote.default.uri.0"));
}
@Test
void reusesUsernameSslAndTimeoutWithoutCopyingCredentialsToPropertiesFiles() {
StandardEnvironment environment = new StandardEnvironment();
Map<String, Object> properties = new HashMap<>();
properties.put("spring.redis.host", "secure.redis");
properties.put("spring.redis.username", "cache-user");
properties.put("spring.redis.password", "secret");
properties.put("spring.redis.ssl", "true");
properties.put("spring.redis.timeout", "2500ms");
environment.getPropertySources().addFirst(new MapPropertySource("test", properties));
new JetCacheRedisEnvironmentPostProcessor().postProcessEnvironment(environment, null);
assertEquals("rediss://cache-user:secret@secure.redis:6379/0",
environment.getProperty("jetcache.remote.default.uri.0"));
assertEquals("2500", environment.getProperty(
"jetcache.remote.default.asyncResultTimeoutInMillis"));
}
}
@@ -0,0 +1,99 @@
package com.config;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.core.io.ClassPathResource;
import java.io.InputStream;
import java.util.Properties;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
class NormalizationCachePropertiesTest {
@Test
void fallbackDefaultsRemainConservative() throws Exception {
NormalizationOptimizationProperties properties = new NormalizationOptimizationProperties();
properties.afterPropertiesSet();
assertFalse(properties.isJetcacheEnabled());
assertFalse(properties.getRuleSourceCache().isEnabled());
assertFalse(properties.getCompiledRule().isEnabled());
assertEquals(1000, properties.getCompiledRule().getShadowTimeoutMs());
assertEquals(30, properties.getRuleSourceCache().getLocalTtlSeconds());
assertEquals(300, properties.getRuleSourceCache().getRemoteTtlSeconds());
assertEquals(10, properties.getRuleSourceCache().getEmptyTtlSeconds());
assertEquals(60, properties.getEnrichmentCache().getLocalTtlSeconds());
assertEquals(3600, properties.getEnrichmentCache().getRemoteTtlSeconds());
}
@Test
void everyRuntimeProfileEnablesTheOptimizedPath() throws Exception {
String[] resources = {
"application.properties",
"application-dev.properties",
"application-test.properties",
"application-prod.properties",
"application-prod-zc.properties",
"application-pre-nw.properties"
};
for (String resource : resources) {
Properties values = new Properties();
try (InputStream input = new ClassPathResource(resource).getInputStream()) {
values.load(input);
}
assertEquals("true", values.getProperty("normalization.jetcache.enabled"), resource);
assertEquals("true", values.getProperty("normalization.rule-source-cache.enabled"), resource);
assertEquals("true", values.getProperty("normalization.compiled-rule.enabled"), resource);
assertEquals("false", values.getProperty(
"normalization.compiled-rule.shadow-compare-enabled"), resource);
assertEquals("0", values.getProperty(
"normalization.compiled-rule.shadow-sample-rate"), resource);
assertEquals("true", values.getProperty(
"normalization.compiled-rule.rule-detail-log-enabled"), resource);
assertEquals("true", values.getProperty(
"normalization.batch-enrichment-cache.enabled"), resource);
assertEquals("true", values.getProperty(
"normalization.message-parse-cache.enabled"), resource);
NormalizationOptimizationProperties bound = new Binder(
new MapConfigurationPropertySource(values))
.bind("normalization", Bindable.of(NormalizationOptimizationProperties.class))
.orElseThrow(() -> new AssertionError("无法绑定 " + resource));
bound.afterPropertiesSet();
assertEquals(true, bound.isJetcacheEnabled(), resource);
assertEquals(true, bound.getRuleSourceCache().isEnabled(), resource);
assertEquals(true, bound.getCompiledRule().isEnabled(), resource);
assertEquals(false, bound.getCompiledRule().isShadowCompareEnabled(), resource);
assertEquals(0.0d, bound.getCompiledRule().getShadowSampleRate(), resource);
assertEquals(true, bound.getCompiledRule().isRuleDetailLogEnabled(), resource);
assertEquals(true, bound.getBatchEnrichmentCache().isEnabled(), resource);
assertEquals(true, bound.getMessageParseCache().isEnabled(), resource);
}
}
@Test
void rejectsPartialJetCacheEnablement() {
NormalizationOptimizationProperties rules = new NormalizationOptimizationProperties();
rules.getRuleSourceCache().setEnabled(true);
assertThrows(IllegalStateException.class, rules::afterPropertiesSet);
NormalizationOptimizationProperties enrichment = new NormalizationOptimizationProperties();
enrichment.getBatchEnrichmentCache().setEnabled(true);
assertThrows(IllegalStateException.class, enrichment::afterPropertiesSet);
}
@Test
void usesJetCache278EncoderAndDecoderPropertyNames() throws Exception {
Properties values = new Properties();
try (InputStream input = new ClassPathResource("application.properties").getInputStream()) {
values.load(input);
}
assertEquals("KRYO5", values.getProperty("jetcache.remote.default.valueEncoder"));
assertEquals("KRYO5", values.getProperty("jetcache.remote.default.valueDecoder"));
assertNull(values.getProperty("jetcache.remote.default.serialPolicy"));
}
}
@@ -0,0 +1,68 @@
package com.config;
import org.apache.kafka.common.TopicPartition;
import org.junit.jupiter.api.Test;
import org.springframework.kafka.config.KafkaListenerEndpointRegistry;
import org.springframework.kafka.listener.ConcurrentMessageListenerContainer;
import org.springframework.kafka.listener.KafkaMessageListenerContainer;
import java.util.Arrays;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class RawKafkaListenerRuntimeVerifierTest {
@Test
void acceptsThreeConfiguredAndCreatedRawConsumers() {
KafkaListenerEndpointRegistry registry = mock(KafkaListenerEndpointRegistry.class);
ConcurrentMessageListenerContainer<Object, Object> container = concurrentContainer(3, 3);
when(registry.getListenerContainer(RawKafkaListenerRuntimeVerifier.RAW_LISTENER_ID))
.thenReturn(container);
RawKafkaListenerRuntimeVerifier verifier =
new RawKafkaListenerRuntimeVerifier(registry, 3, true);
assertTrue(verifier.verifyNow());
}
@Test
void detectsWhenOnlyOneRawConsumerWasActuallyCreated() {
KafkaListenerEndpointRegistry registry = mock(KafkaListenerEndpointRegistry.class);
ConcurrentMessageListenerContainer<Object, Object> container = concurrentContainer(3, 1);
when(registry.getListenerContainer(RawKafkaListenerRuntimeVerifier.RAW_LISTENER_ID))
.thenReturn(container);
RawKafkaListenerRuntimeVerifier verifier =
new RawKafkaListenerRuntimeVerifier(registry, 3, true);
assertFalse(verifier.verifyNow());
}
@Test
void disabledRawConsumerDoesNotRequireAContainer() {
RawKafkaListenerRuntimeVerifier verifier = new RawKafkaListenerRuntimeVerifier(
mock(KafkaListenerEndpointRegistry.class), 3, false);
assertTrue(verifier.verifyNow());
}
@SuppressWarnings("unchecked")
private ConcurrentMessageListenerContainer<Object, Object> concurrentContainer(
int declaredConcurrency, int childCount) {
ConcurrentMessageListenerContainer<Object, Object> container =
mock(ConcurrentMessageListenerContainer.class);
when(container.getConcurrency()).thenReturn(declaredConcurrency);
KafkaMessageListenerContainer<Object, Object>[] children =
new KafkaMessageListenerContainer[childCount];
for (int i = 0; i < childCount; i++) children[i] = mock(KafkaMessageListenerContainer.class);
when(container.getContainers()).thenReturn(Arrays.asList(children));
when(container.getAssignedPartitions()).thenReturn(
Collections.singletonList(new TopicPartition("agent-01-syslog-topic", 0)));
when(container.getAssignmentsByClientId()).thenReturn(Collections.emptyMap());
return container;
}
}
@@ -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)"));
}
}