feat(syslog): 优化大日志通道并增加异常连续消费兜底

- 128 KiB 及以上日志默认进入大日志 Topic
- InfluxDB 原文及达梦大字段超过 256 KiB 时截断保存
- 截断内容追加 HB_XDR_TRUNCATED 标记
- InfluxDB 写入失败时继续执行规则解析和达梦写入
- 未知或系统性异常优先写入普通/大日志 DLT
- DLT 写入失败时记录特殊告警并提交 offset,避免阻塞后续消息
- DLT 失败等待时间默认限制为 1 秒
- 增加连续性放行、DLT 成功及失败监控指标
- 更新 Docker Compose、v3 升级文档及相关测试
This commit is contained in:
王绍宏
2026-09-12 19:22:52 +08:00
parent 3b6c9e4714
commit 0e9e6656db
64 changed files with 3189 additions and 63 deletions
@@ -121,6 +121,56 @@ class SyslogWalDispatcherTest {
verify(producer, never()).messagePush(any(), anyString(), anyString(), anyString(), any());
}
@Test
void largeRecordUsesDedicatedKafkaLaneAndReleasesByteBudgetAfterAck() throws Exception {
AppProperties properties = properties(tempDirectory);
properties.setSyslogLargeMessageThresholdBytes(4L);
properties.setSyslogMessageMaxBytes(32L);
properties.setSyslogWalMaxPendingBytes(64L);
properties.setSyslogDispatchMaxInflightBytes(64L);
wal = new SyslogWalService(properties, new ObjectMapper());
wal.initialize();
wal.tryAppend(event("20260811-large", "12345"));
waitUntil(() -> wal.getAppendedCount() == 1L);
DeviceDevice device = new DeviceDevice();
device.setId(126);
DeviceContextResolver resolver = mock(DeviceContextResolver.class);
when(resolver.resolve(anyString(), anyString()))
.thenReturn(DeviceContextResolver.Resolution.registered(device));
kafkaProducer producer = mock(kafkaProducer.class);
when(producer.messagePushLarge(any(DeviceDevice.class), anyString(), anyString(),
anyString(), anyString(), any()))
.thenReturn(CompletableFuture.completedFuture(null));
dispatcher = new SyslogWalDispatcher(wal, resolver, producer, properties);
dispatcher.start();
waitUntil(() -> wal.getCheckpointSequence() == 1L);
verify(producer).messagePushLarge(eq(device), eq("12345"),
eq("20260811200000000"), eq("20260811200000000"),
eq("20260811-large"), eq(null));
verify(producer, never()).messagePush(any(), anyString(), anyString(), anyString(), any());
assertEquals(1L, dispatcher.getLargeDispatched());
assertEquals(0L, dispatcher.getInflightBytes());
}
@Test
void singleRecordLargerThanByteBudgetCanRunExclusivelyWhenHardLimitIsDisabled() throws Exception {
SyslogWalDispatcher.ByteBudget budget = new SyslogWalDispatcher.ByteBudget(4L);
budget.acquire(5L);
assertEquals(5L, budget.used());
budget.release(5L);
assertEquals(0L, budget.used());
}
@Test
void utf8BudgetCountsChineseAndEmojiWithoutAllocatingEncodedPayload() {
assertEquals(11L, SyslogWalDispatcher.utf8Length("中文🙂x"));
}
private SyslogIngressEvent event(String id, String raw) {
return new SyslogIngressEvent(id, LocalDateTime.of(2026, 8, 11, 20, 0),
"192.168.4.19", 514, "TCP", raw);
@@ -147,6 +197,7 @@ class SyslogWalDispatcherTest {
value.setSyslogCharset("UTF-8");
value.setSyslogDispatchWorkerThreads(2);
value.setSyslogDispatchMaxInflight(8);
value.setSyslogDispatchMaxInflightBytes(0L);
value.setSyslogDispatchRetryBackoffMs(10);
value.setSyslogDispatchShutdownTimeoutMs(2000);
value.setSyslogDeviceContextCacheTtlMs(60000);
@@ -98,6 +98,58 @@ class SyslogWalServiceTest {
assertEquals(null, attempt.getDurableStage());
}
@Test
void oversizedMessageIsRejectedBeforeEnteringWal() {
AppProperties properties = properties(tempDirectory);
properties.setSyslogMessageMaxBytes(4L);
properties.setSyslogWalMaxPendingBytes(16L);
service = new SyslogWalService(properties, new ObjectMapper());
service.initialize();
SyslogWalService.AdmissionResult result = service.tryAppend(
event("20260811-too-large", "12345"));
assertEquals(SyslogWalService.AdmissionResult.OVERSIZED, result);
assertEquals(1L, service.getOversizedCount());
assertEquals(0, service.getQueueSize());
assertEquals(0L, service.getPendingAppendBytes());
}
@Test
void pendingByteBudgetBackpressuresEvenWhenRecordQueueHasCapacity() {
AppProperties properties = properties(tempDirectory);
properties.setSyslogMessageMaxBytes(5L);
properties.setSyslogWalMaxPendingBytes(5L);
properties.setSyslogWalFlushRecords(100);
properties.setSyslogWalFlushIntervalMs(10000L);
service = new SyslogWalService(properties, new ObjectMapper());
service.initialize();
assertEquals(SyslogWalService.AdmissionResult.ACCEPTED,
service.tryAppend(event("20260811-first", "12345")));
assertEquals(SyslogWalService.AdmissionResult.BACKPRESSURE,
service.tryAppend(event("20260811-second", "x")));
assertEquals(5L, service.getPendingAppendBytes());
assertEquals(1L, service.getBackpressureCount());
}
@Test
void disabledHardLimitLetsOneOverBudgetRecordMakeProgressExclusively() {
AppProperties properties = properties(tempDirectory);
properties.setSyslogMessageMaxBytes(0L);
properties.setSyslogWalMaxPendingBytes(4L);
properties.setSyslogWalFlushRecords(100);
properties.setSyslogWalFlushIntervalMs(10000L);
service = new SyslogWalService(properties, new ObjectMapper());
service.initialize();
assertEquals(SyslogWalService.AdmissionResult.ACCEPTED,
service.tryAppend(event("20260811-over-budget", "12345")));
assertEquals(SyslogWalService.AdmissionResult.BACKPRESSURE,
service.tryAppend(event("20260811-waits", "x")));
assertEquals(5L, service.getPendingAppendBytes());
}
@Test
void forceBeforeCursorAndPersistSchemaV2() throws Exception {
service = new SyslogWalService(properties(tempDirectory), new ObjectMapper());
@@ -63,6 +63,20 @@ class KafkaProducerEventIdTest {
assertEquals("126", record.key());
}
@Test
void largeLaneUsesDedicatedTopicWithoutChangingPartitionKey() {
AppProperties properties = new AppProperties();
properties.setKafkaProducerTopic("raw");
properties.setKafkaProducerLargeTopic("raw.large");
kafkaProducer producer = new kafkaProducer(properties, mock(Producer.class), "event_id");
ProducerRecord<String, String> record = producer.buildRecord(
properties.getKafkaProducerLargeTopic(), 126, "20260811-event", "ciphertext");
assertEquals("raw.large", record.topic());
assertEquals("20260811-event", record.key());
}
@Test
void invalidPartitionKeyStrategyFailsFast() {
assertThrows(IllegalArgumentException.class, () -> producer("random"));