feat(syslog): 增加普通通道字段截断并完善慢写诊断
- 新增普通通道字段上限配置,默认2048个UTF-8字节,截断尾部保留标记 - 规则计算后执行截断,告警不应用新增2KB限制,原有256KB策略不变 - 非标原文、头部及详细原因应用新增限制,大日志通道和InfluxDB策略不变 - Retry/Replay按原始Topic识别通道,启动日志输出生效上限 - 补充达梦实际写入内容大小、最大行及最大字段诊断 - 慢日志默认阈值统一调整为1000毫秒,异常和失败仍记录 - 更新v4配置说明及回归测试 验证:JDK 8构建成功,250项测试通过 升级:替换JAR并重启,无需数据库或Kafka脚本
This commit is contained in:
+16
-2
@@ -8,6 +8,7 @@ import com.common.util.SyslogParser;
|
|||||||
import com.config.AppProperties;
|
import com.config.AppProperties;
|
||||||
import com.config.KafkaConsumerProperties;
|
import com.config.KafkaConsumerProperties;
|
||||||
import com.common.service.DmStandardizedBatchWriter;
|
import com.common.service.DmStandardizedBatchWriter;
|
||||||
|
import com.common.service.RawLaneFieldLimiter;
|
||||||
import com.common.service.DirectStatsProjectionWriter;
|
import com.common.service.DirectStatsProjectionWriter;
|
||||||
import com.common.service.SyslogNonNormalMessageService;
|
import com.common.service.SyslogNonNormalMessageService;
|
||||||
import com.common.service.SyslogNormalDataService;
|
import com.common.service.SyslogNormalDataService;
|
||||||
@@ -54,6 +55,9 @@ import com.common.util.Sm4Util;
|
|||||||
@Component
|
@Component
|
||||||
public class SysLogProcessor {
|
public class SysLogProcessor {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RawLaneFieldLimiter rawLaneFieldLimiter;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("logNormalProcessorExecutor")
|
@Qualifier("logNormalProcessorExecutor")
|
||||||
private Executor taskExecutor;
|
private Executor taskExecutor;
|
||||||
@@ -85,7 +89,7 @@ public class SysLogProcessor {
|
|||||||
@Value("${syslog.failure-continuity.enabled:true}")
|
@Value("${syslog.failure-continuity.enabled:true}")
|
||||||
private boolean failureContinuityEnabled;
|
private boolean failureContinuityEnabled;
|
||||||
|
|
||||||
@Value("${syslog.slow-batch.threshold-ms:500}")
|
@Value("${syslog.slow-batch.threshold-ms:1000}")
|
||||||
private long slowBatchThresholdMs;
|
private long slowBatchThresholdMs;
|
||||||
|
|
||||||
/** listener、业务处理和 Kafka commit 均在同一个 consumer 线程,ThreadLocal 用于拼接端到端慢批次。 */
|
/** listener、业务处理和 Kafka commit 均在同一个 consumer 线程,ThreadLocal 用于拼接端到端慢批次。 */
|
||||||
@@ -1120,15 +1124,25 @@ public class SysLogProcessor {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean limitRawFields = rawLaneFieldLimiter.appliesTo(prepared.message.record.topic(),
|
||||||
|
prepared.message.context == null ? null : prepared.message.context.getOriginalTopic());
|
||||||
if (retained.keepNonNormal) {
|
if (retained.keepNonNormal) {
|
||||||
|
if (limitRawFields) {
|
||||||
|
prepared.result.getNonNormalMessages().forEach(rawLaneFieldLimiter::truncateNonNormal);
|
||||||
|
}
|
||||||
nonNormalMessages.addAll(prepared.result.getNonNormalMessages());
|
nonNormalMessages.addAll(prepared.result.getNonNormalMessages());
|
||||||
}
|
}
|
||||||
int retainedStandardRows = 0;
|
int retainedStandardRows = 0;
|
||||||
for (Map<String, Object> row : prepared.result.getRows()) {
|
for (Map<String, Object> row : prepared.result.getRows()) {
|
||||||
boolean alarm = syslogNormalDataService.isAlarmData(row);
|
boolean alarm = syslogNormalDataService.isAlarmData(row);
|
||||||
if (alarm || retained.keepNormal) {
|
if (alarm || retained.keepNormal) {
|
||||||
|
Map<String, Object> storedRow = row;
|
||||||
|
if (limitRawFields && !alarm) {
|
||||||
|
storedRow = new LinkedHashMap<>(row);
|
||||||
|
rawLaneFieldLimiter.truncateRow(storedRow);
|
||||||
|
}
|
||||||
DmStandardizedBatchWriter.WriteRequest<PreparedMessage> request =
|
DmStandardizedBatchWriter.WriteRequest<PreparedMessage> request =
|
||||||
new DmStandardizedBatchWriter.WriteRequest<>(prepared.message, row);
|
new DmStandardizedBatchWriter.WriteRequest<>(prepared.message, storedRow);
|
||||||
if (alarm) alarmWriteRequests.add(request);
|
if (alarm) alarmWriteRequests.add(request);
|
||||||
else normalWriteRequests.add(request);
|
else normalWriteRequests.add(request);
|
||||||
retainedStandardRows++;
|
retainedStandardRows++;
|
||||||
|
|||||||
+14
-1
@@ -46,20 +46,33 @@ public class DmPreparedBatchInserter {
|
|||||||
|
|
||||||
public BatchExecution insert(boolean alarmTable, List<Map<String, Object>> rows,
|
public BatchExecution insert(boolean alarmTable, List<Map<String, Object>> rows,
|
||||||
int statementTimeoutSeconds) {
|
int statementTimeoutSeconds) {
|
||||||
|
return insert(alarmTable, rows, statementTimeoutSeconds, new DmWriteSizeEstimate());
|
||||||
|
}
|
||||||
|
|
||||||
|
BatchExecution insert(boolean alarmTable, List<Map<String, Object>> rows,
|
||||||
|
int statementTimeoutSeconds, DmWriteSizeEstimate writeSize) {
|
||||||
if (rows == null || rows.isEmpty()) {
|
if (rows == null || rows.isEmpty()) {
|
||||||
return BatchExecution.EMPTY;
|
return BatchExecution.EMPTY;
|
||||||
}
|
}
|
||||||
final String table = alarmTable ? ALARM_TABLE : NORMAL_TABLE;
|
final String table = alarmTable ? ALARM_TABLE : NORMAL_TABLE;
|
||||||
return jdbcTemplate.execute((ConnectionCallback<BatchExecution>) connection ->
|
return jdbcTemplate.execute((ConnectionCallback<BatchExecution>) connection ->
|
||||||
execute(connection, table, rows, statementTimeoutSeconds));
|
execute(connection, table, rows, statementTimeoutSeconds, writeSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
BatchExecution execute(Connection connection, String table, List<Map<String, Object>> rows,
|
BatchExecution execute(Connection connection, String table, List<Map<String, Object>> rows,
|
||||||
int statementTimeoutSeconds) throws SQLException {
|
int statementTimeoutSeconds) throws SQLException {
|
||||||
|
return execute(connection, table, rows, statementTimeoutSeconds, new DmWriteSizeEstimate());
|
||||||
|
}
|
||||||
|
|
||||||
|
BatchExecution execute(Connection connection, String table, List<Map<String, Object>> rows,
|
||||||
|
int statementTimeoutSeconds, DmWriteSizeEstimate writeSize) throws SQLException {
|
||||||
long planStarted = System.nanoTime();
|
long planStarted = System.nanoTime();
|
||||||
TableMetadata metadata = metadata(connection, table);
|
TableMetadata metadata = metadata(connection, table);
|
||||||
BatchPlan plan = buildPlan(table, rows, metadata);
|
BatchPlan plan = buildPlan(table, rows, metadata);
|
||||||
long planMs = elapsedMillis(planStarted);
|
long planMs = elapsedMillis(planStarted);
|
||||||
|
// 使用元数据白名单选中的列,排除规则携带但不会实际绑定的字段。
|
||||||
|
// 执行前完成测量,使JDBC绑定/执行失败时仍可输出这次尝试的数据量。
|
||||||
|
writeSize.measure(rows, plan.columns);
|
||||||
|
|
||||||
long bindMs;
|
long bindMs;
|
||||||
long executeMs;
|
long executeMs;
|
||||||
|
|||||||
+9
-8
@@ -36,7 +36,7 @@ public class DmStandardizedBatchExecutor {
|
|||||||
@org.springframework.beans.factory.annotation.Value("${stats.direct.transaction-log-enabled:false}")
|
@org.springframework.beans.factory.annotation.Value("${stats.direct.transaction-log-enabled:false}")
|
||||||
private boolean transactionLogEnabled;
|
private boolean transactionLogEnabled;
|
||||||
|
|
||||||
@org.springframework.beans.factory.annotation.Value("${syslog.slow-batch.threshold-ms:500}")
|
@org.springframework.beans.factory.annotation.Value("${syslog.slow-batch.threshold-ms:1000}")
|
||||||
private long slowTransactionThresholdMs;
|
private long slowTransactionThresholdMs;
|
||||||
|
|
||||||
@org.springframework.beans.factory.annotation.Value("${stats.direct.max-concurrency-retries:1}")
|
@org.springframework.beans.factory.annotation.Value("${stats.direct.max-concurrency-retries:1}")
|
||||||
@@ -118,7 +118,7 @@ public class DmStandardizedBatchExecutor {
|
|||||||
// 事实表使用一个固定 PreparedStatement 直接 addBatch,彻底绕开逐行 OGNL 动态 SQL。
|
// 事实表使用一个固定 PreparedStatement 直接 addBatch,彻底绕开逐行 OGNL 动态 SQL。
|
||||||
timing.phase("JDBC_FACT_WRITE");
|
timing.phase("JDBC_FACT_WRITE");
|
||||||
timing.batchExecution = preparedBatchInserter.insert(
|
timing.batchExecution = preparedBatchInserter.insert(
|
||||||
alarmTable, rows, transactionTimeoutSeconds);
|
alarmTable, rows, transactionTimeoutSeconds, timing.writeSize);
|
||||||
|
|
||||||
// 兼容期变更日志仍与事实行共用当前事务;默认关闭时完全不创建 MyBatis BatchSession。
|
// 兼容期变更日志仍与事实行共用当前事务;默认关闭时完全不创建 MyBatis BatchSession。
|
||||||
if (statsChangeCaptureService.isEnabled()) {
|
if (statsChangeCaptureService.isEnabled()) {
|
||||||
@@ -145,11 +145,11 @@ public class DmStandardizedBatchExecutor {
|
|||||||
} catch (RuntimeException failure) {
|
} catch (RuntimeException failure) {
|
||||||
log.error("[SLOW_DM_TRANSACTION] batchId={}, table={}, factRows={}, status=FAILED, "
|
log.error("[SLOW_DM_TRANSACTION] batchId={}, table={}, factRows={}, status=FAILED, "
|
||||||
+ "failureStage={}, phaseElapsedMs={}, transactionMs={}, connectionBeginMs={}, "
|
+ "failureStage={}, phaseElapsedMs={}, transactionMs={}, connectionBeginMs={}, "
|
||||||
+ "jdbcExecuteMs={}, lateAlarmBucketMs={}, retry={}, errorType={}",
|
+ "jdbcExecuteMs={}, lateAlarmBucketMs={}, retry={}, errorType={}, {}",
|
||||||
org.slf4j.MDC.get("syslogBatchId"), alarmTable ? "syslog_normal_alarm" : "syslog_normal_data",
|
org.slf4j.MDC.get("syslogBatchId"), alarmTable ? "syslog_normal_alarm" : "syslog_normal_data",
|
||||||
rows.size(), timing.phase, elapsedMs(timing.phaseStarted), elapsedMs(transactionStarted),
|
rows.size(), timing.phase, elapsedMs(timing.phaseStarted), elapsedMs(transactionStarted),
|
||||||
timing.beginMs, timing.batchExecution.getExecuteMs(), timing.lateBucketMs, retries,
|
timing.beginMs, timing.batchExecution.getExecuteMs(), timing.lateBucketMs, retries,
|
||||||
failure.getClass().getName());
|
failure.getClass().getName(), timing.writeSize);
|
||||||
throw failure;
|
throw failure;
|
||||||
}
|
}
|
||||||
long transactionMs = elapsedMs(transactionStarted);
|
long transactionMs = elapsedMs(transactionStarted);
|
||||||
@@ -159,22 +159,22 @@ public class DmStandardizedBatchExecutor {
|
|||||||
if (slowTransactionThresholdMs > 0 && transactionMs >= slowTransactionThresholdMs) {
|
if (slowTransactionThresholdMs > 0 && transactionMs >= slowTransactionThresholdMs) {
|
||||||
log.warn("[SLOW_DM_TRANSACTION] batchId={}, table={}, factRows={}, status=SUCCESS, connectionBeginMs={}, jdbcPlanMs={}, "
|
log.warn("[SLOW_DM_TRANSACTION] batchId={}, table={}, factRows={}, status=SUCCESS, connectionBeginMs={}, jdbcPlanMs={}, "
|
||||||
+ "jdbcBindMs={}, jdbcExecuteMs={}, lateAlarmBucketMs={}, statsProjectionStageMs={}, "
|
+ "jdbcBindMs={}, jdbcExecuteMs={}, lateAlarmBucketMs={}, statsProjectionStageMs={}, "
|
||||||
+ "commitMs={}, transactionMs={}, retry={}",
|
+ "commitMs={}, transactionMs={}, retry={}, {}",
|
||||||
org.slf4j.MDC.get("syslogBatchId"), alarmTable ? "syslog_normal_alarm" : "syslog_normal_data", rows.size(), timing.beginMs,
|
org.slf4j.MDC.get("syslogBatchId"), alarmTable ? "syslog_normal_alarm" : "syslog_normal_data", rows.size(), timing.beginMs,
|
||||||
timing.batchExecution.getPlanMs(), timing.batchExecution.getBindMs(),
|
timing.batchExecution.getPlanMs(), timing.batchExecution.getBindMs(),
|
||||||
timing.batchExecution.getExecuteMs(), timing.lateBucketMs,
|
timing.batchExecution.getExecuteMs(), timing.lateBucketMs,
|
||||||
timing.statsResult == null ? 0L : timing.statsResult.getMergeMs(),
|
timing.statsResult == null ? 0L : timing.statsResult.getMergeMs(),
|
||||||
timing.commitMs, transactionMs, retries);
|
timing.commitMs, transactionMs, retries, timing.writeSize);
|
||||||
}
|
}
|
||||||
if (transactionLogEnabled && timing.statsResult != null && timing.statsResult.isDirect()) {
|
if (transactionLogEnabled && timing.statsResult != null && timing.statsResult.isDirect()) {
|
||||||
log.info("DIRECT 达梦事务完成: factRows={}, deviceGroups={}, ruleGroups={}, aggregateMs={}, " +
|
log.info("DIRECT 达梦事务完成: factRows={}, deviceGroups={}, ruleGroups={}, aggregateMs={}, " +
|
||||||
"jdbcPlanMs={}, jdbcBindMs={}, jdbcExecuteMs={}, factFlushMs={}, "
|
"jdbcPlanMs={}, jdbcBindMs={}, jdbcExecuteMs={}, factFlushMs={}, "
|
||||||
+ "statsProjectionStageMs={}, materializeMs={}, commitMs={}, transactionMs={}, retry={}",
|
+ "statsProjectionStageMs={}, materializeMs={}, commitMs={}, transactionMs={}, retry={}, {}",
|
||||||
rows.size(), timing.statsResult.getDeviceGroups(), timing.statsResult.getRuleGroups(),
|
rows.size(), timing.statsResult.getDeviceGroups(), timing.statsResult.getRuleGroups(),
|
||||||
timing.statsResult.getAggregateMs(), timing.batchExecution.getPlanMs(),
|
timing.statsResult.getAggregateMs(), timing.batchExecution.getPlanMs(),
|
||||||
timing.batchExecution.getBindMs(), timing.batchExecution.getExecuteMs(), timing.factFlushMs,
|
timing.batchExecution.getBindMs(), timing.batchExecution.getExecuteMs(), timing.factFlushMs,
|
||||||
timing.statsResult.getMergeMs(), timing.statsResult.getMaterializeMs(),
|
timing.statsResult.getMergeMs(), timing.statsResult.getMaterializeMs(),
|
||||||
timing.commitMs, transactionMs, retries);
|
timing.commitMs, transactionMs, retries, timing.writeSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +189,7 @@ public class DmStandardizedBatchExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final class TransactionTiming {
|
private static final class TransactionTiming {
|
||||||
|
private final DmWriteSizeEstimate writeSize = new DmWriteSizeEstimate();
|
||||||
private String phase = "CONNECTION_BEGIN";
|
private String phase = "CONNECTION_BEGIN";
|
||||||
private long phaseStarted = System.nanoTime();
|
private long phaseStarted = System.nanoTime();
|
||||||
private void phase(String value) { phase = value; phaseStarted = System.nanoTime(); }
|
private void phase(String value) { phase = value; phaseStarted = System.nanoTime(); }
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@ public class DmStandardizedBatchWriter {
|
|||||||
@Value("${dm.standardized-write.batch-size:500}")
|
@Value("${dm.standardized-write.batch-size:500}")
|
||||||
private int batchSize;
|
private int batchSize;
|
||||||
|
|
||||||
@Value("${syslog.slow-batch.threshold-ms:500}")
|
@Value("${syslog.slow-batch.threshold-ms:1000}")
|
||||||
private long slowQueueThresholdMs;
|
private long slowQueueThresholdMs;
|
||||||
|
|
||||||
@Value("${dm.standardized-write.transaction-timeout-seconds:45}")
|
@Value("${dm.standardized-write.transaction-timeout-seconds:45}")
|
||||||
|
|||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
package com.common.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JDBC参数内容大小估算,不是网络包、数据库页或REDO大小。
|
||||||
|
* 只统计实际INSERT字段,调用位置在截断/字段准备之后、绑定/执行之前。
|
||||||
|
* 不读取LOB或流,不序列化任意对象,不保留字段值,不创建大字符串的UTF-8副本。
|
||||||
|
*/
|
||||||
|
final class DmWriteSizeEstimate {
|
||||||
|
private String status = "NOT_MEASURED";
|
||||||
|
private long payloadBytes;
|
||||||
|
private long maxRowBytes;
|
||||||
|
private long maxFieldBytes;
|
||||||
|
private String maxFieldName = "none";
|
||||||
|
private long unmeasuredValues;
|
||||||
|
private long elapsedMs;
|
||||||
|
|
||||||
|
void measure(List<Map<String, Object>> rows, List<DmPreparedBatchInserter.ColumnMetadata> columns) {
|
||||||
|
long started = System.nanoTime();
|
||||||
|
try {
|
||||||
|
for (Map<String, Object> row : rows) {
|
||||||
|
long rowBytes = 0L;
|
||||||
|
for (DmPreparedBatchInserter.ColumnMetadata column : columns) {
|
||||||
|
long bytes = valueBytes(row.get(column.name));
|
||||||
|
if (bytes < 0L) {
|
||||||
|
unmeasuredValues++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rowBytes += bytes;
|
||||||
|
if (bytes > maxFieldBytes) {
|
||||||
|
maxFieldBytes = bytes;
|
||||||
|
maxFieldName = column.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payloadBytes += rowBytes;
|
||||||
|
maxRowBytes = Math.max(maxRowBytes, rowBytes);
|
||||||
|
}
|
||||||
|
status = unmeasuredValues == 0L ? "COMPLETE" : "PARTIAL";
|
||||||
|
} catch (RuntimeException diagnosticFailure) {
|
||||||
|
// 观测失败不能改变事实写入行为;不记录异常消息,避免携带字段内容。
|
||||||
|
status = "FAILED";
|
||||||
|
} finally {
|
||||||
|
elapsedMs = (System.nanoTime() - started) / 1_000_000L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static long valueBytes(Object value) {
|
||||||
|
if (value == null) return 0L;
|
||||||
|
if (value instanceof String) return utf8Length((String) value);
|
||||||
|
if (value instanceof byte[]) return ((byte[]) value).length;
|
||||||
|
// 只对已知标量计其文本表示,避免任意对象toString触发IO或泄漏内容。
|
||||||
|
Class<?> type = value.getClass();
|
||||||
|
if (type == Boolean.class || type == Character.class || type == Byte.class
|
||||||
|
|| type == Short.class || type == Integer.class || type == Long.class
|
||||||
|
|| type == Float.class || type == Double.class || type == BigDecimal.class
|
||||||
|
|| type == BigInteger.class || type == LocalDate.class || type == LocalDateTime.class
|
||||||
|
|| type == Timestamp.class || type == java.sql.Date.class || type == java.sql.Time.class) {
|
||||||
|
return utf8Length(value.toString());
|
||||||
|
}
|
||||||
|
if (type == java.util.Date.class) {
|
||||||
|
return utf8Length(new Timestamp(((java.util.Date) value).getTime()).toString());
|
||||||
|
}
|
||||||
|
return -1L;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 与JDK8 String.getBytes(UTF_8)一致:有效代理对4字节,孤立代理字符替换为一个问号字节。 */
|
||||||
|
static long utf8Length(String text) {
|
||||||
|
long bytes = 0L;
|
||||||
|
for (int i = 0; i < text.length(); i++) {
|
||||||
|
char ch = text.charAt(i);
|
||||||
|
if (ch < 0x80) bytes++;
|
||||||
|
else if (ch < 0x800) bytes += 2L;
|
||||||
|
else if (Character.isHighSurrogate(ch) && i + 1 < text.length()
|
||||||
|
&& Character.isLowSurrogate(text.charAt(i + 1))) {
|
||||||
|
bytes += 4L;
|
||||||
|
i++;
|
||||||
|
} else if (Character.isSurrogate(ch)) bytes++;
|
||||||
|
else bytes += 3L;
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
long getPayloadBytes() { return payloadBytes; }
|
||||||
|
long getMaxRowBytes() { return maxRowBytes; }
|
||||||
|
long getMaxFieldBytes() { return maxFieldBytes; }
|
||||||
|
String getMaxFieldName() { return maxFieldName; }
|
||||||
|
long getUnmeasuredValues() { return unmeasuredValues; }
|
||||||
|
String getStatus() { return status; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "sizeEstimateStatus=" + status
|
||||||
|
+ ", estimatedPayloadBytes=" + payloadBytes
|
||||||
|
+ ", maxRowEstimatedBytes=" + maxRowBytes
|
||||||
|
+ ", maxFieldEstimatedBytes=" + maxFieldBytes
|
||||||
|
+ ", maxFieldName=" + maxFieldName
|
||||||
|
+ ", unmeasuredValues=" + unmeasuredValues
|
||||||
|
+ ", sizeEstimateMs=" + elapsedMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
package com.common.service;
|
||||||
|
|
||||||
|
import com.common.entity.SyslogNonNormalMessage;
|
||||||
|
import com.common.util.Utf8TextTruncator;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/** 普通来源消息完成规则和采样后,限制非告警达梦输出文本;调用方排除告警,不修改规则输入和InfluxDB原文。 */
|
||||||
|
@Component
|
||||||
|
public class RawLaneFieldLimiter {
|
||||||
|
// 这些字段参与幂等、路由、分区或关联,不允许截断改变其语义。
|
||||||
|
private static final Set<String> IDENTITY_FIELDS = new HashSet<>(Arrays.asList(
|
||||||
|
"id", "syslog_uuid", "syslog_topic", "normalize_rule_id", "rule_version",
|
||||||
|
"device_id", "tenant_id", "agent_id", "origin_agent_id", "collect_task_id",
|
||||||
|
"log_time", "created_at", "receive_time", "event_level"));
|
||||||
|
|
||||||
|
private final long maxBytes;
|
||||||
|
private final String rawTopic;
|
||||||
|
|
||||||
|
public RawLaneFieldLimiter(
|
||||||
|
@Value("${syslog.raw-listener.field-max-utf8-bytes:2048}") long maxBytes,
|
||||||
|
@Value("${spring.kafka.consumer.topic}") String rawTopic) {
|
||||||
|
if (maxBytes < 0 || (maxBytes > 0 && maxBytes < 64)) {
|
||||||
|
throw new IllegalArgumentException("syslog.raw-listener.field-max-utf8-bytes 必须为0或至少64");
|
||||||
|
}
|
||||||
|
this.maxBytes = maxBytes;
|
||||||
|
this.rawTopic = rawTopic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean appliesTo(String currentTopic, String originalTopic) {
|
||||||
|
String sourceTopic = originalTopic == null || originalTopic.isEmpty() ? currentTopic : originalTopic;
|
||||||
|
return maxBytes > 0 && rawTopic != null && rawTopic.equals(sourceTopic);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 包括自定义映射文本,不限于旧的大字段白名单;数值、日期、二进制值保持原类型。 */
|
||||||
|
public void truncateRow(Map<String, Object> row) {
|
||||||
|
if (row == null || maxBytes == 0) return;
|
||||||
|
for (Map.Entry<String, Object> entry : row.entrySet()) {
|
||||||
|
if (entry.getValue() instanceof String && !IDENTITY_FIELDS.contains(entry.getKey())) {
|
||||||
|
entry.setValue(truncate((String) entry.getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void truncateNonNormal(SyslogNonNormalMessage message) {
|
||||||
|
if (message == null || maxBytes == 0) return;
|
||||||
|
message.setSyslogMessage(truncate(message.getSyslogMessage()));
|
||||||
|
message.setHeaderMessage(truncate(message.getHeaderMessage()));
|
||||||
|
message.setReasonDetail(truncate(message.getReasonDetail()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String truncate(String value) {
|
||||||
|
// 每个UTF-16代码单元最多占3个UTF-8字节,短字段无需分配UTF-8数组。
|
||||||
|
if (value == null || (long) value.length() * 3L <= maxBytes) return value;
|
||||||
|
return Utf8TextTruncator.truncate(value, maxBytes).getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-2
@@ -12,7 +12,7 @@ import org.springframework.stereotype.Component;
|
|||||||
public class SyslogBuildStartupLogger {
|
public class SyslogBuildStartupLogger {
|
||||||
|
|
||||||
// 确认包含此功能修订,不代表唯一构建;后续功能修订时同步更新此标识。
|
// 确认包含此功能修订,不代表唯一构建;后续功能修订时同步更新此标识。
|
||||||
static final String CODE_REVISION = "ASYNC_STATS_ADAPTIVE_RAW_V20260913_56";
|
static final String CODE_REVISION = "ASYNC_STATS_ADAPTIVE_RAW_V20260913_57";
|
||||||
private final Environment environment;
|
private final Environment environment;
|
||||||
|
|
||||||
public SyslogBuildStartupLogger(Environment environment) {
|
public SyslogBuildStartupLogger(Environment environment) {
|
||||||
@@ -32,12 +32,13 @@ public class SyslogBuildStartupLogger {
|
|||||||
+ ", rawConcurrency=" + value("syslog.raw-listener.concurrency", "3")
|
+ ", rawConcurrency=" + value("syslog.raw-listener.concurrency", "3")
|
||||||
+ ", baseProcessingRecords=" + value("syslog.raw-listener.max-poll-records", "500")
|
+ ", baseProcessingRecords=" + value("syslog.raw-listener.max-poll-records", "500")
|
||||||
+ ", adaptiveMaxRecords=" + value("syslog.raw-listener.adaptive-max-records", "3000")
|
+ ", adaptiveMaxRecords=" + value("syslog.raw-listener.adaptive-max-records", "3000")
|
||||||
|
+ ", rawFieldMaxUtf8Bytes=" + value("syslog.raw-listener.field-max-utf8-bytes", "2048")
|
||||||
+ ", samplingEnabled=" + value("syslog.backlog-sampling.enabled", "true")
|
+ ", samplingEnabled=" + value("syslog.backlog-sampling.enabled", "true")
|
||||||
+ ", statsWriteMode=" + value("stats.write-mode", "DIRECT")
|
+ ", statsWriteMode=" + value("stats.write-mode", "DIRECT")
|
||||||
+ ", ingressStatsEnabled=" + value("stats.ingress-metadata.enabled", "true")
|
+ ", ingressStatsEnabled=" + value("stats.ingress-metadata.enabled", "true")
|
||||||
+ ", statsPublisherEnabled=" + value("stats.projection-async.enabled", "true")
|
+ ", statsPublisherEnabled=" + value("stats.projection-async.enabled", "true")
|
||||||
+ ", lateBucketEnabled=" + value("analysis.late-replay.outbox-enabled", "true")
|
+ ", lateBucketEnabled=" + value("analysis.late-replay.outbox-enabled", "true")
|
||||||
+ ", slowBatchThresholdMs=" + value("syslog.slow-batch.threshold-ms", "500");
|
+ ", slowBatchThresholdMs=" + value("syslog.slow-batch.threshold-ms", "1000");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String value(String key, String fallback) {
|
private String value(String key, String fallback) {
|
||||||
|
|||||||
@@ -82,8 +82,9 @@ spring.kafka.consumer.large-topic=${KAFKA_CONSUMER_LARGE_TOPIC:${spring.kafka.co
|
|||||||
syslog.raw-consumer.enabled=true
|
syslog.raw-consumer.enabled=true
|
||||||
# Raw Topic 当前有 3 个分区,使用独立工厂显式创建 3 个 consumer;可通过环境变量灰度调整。
|
# Raw Topic 当前有 3 个分区,使用独立工厂显式创建 3 个 consumer;可通过环境变量灰度调整。
|
||||||
syslog.raw-listener.concurrency=${SYSLOG_RAW_LISTENER_CONCURRENCY:3}
|
syslog.raw-listener.concurrency=${SYSLOG_RAW_LISTENER_CONCURRENCY:3}
|
||||||
# 端到端批次耗时达到该阈值时,打印业务各阶段及 Kafka offset 同步提交耗时;0 表示关闭。
|
# 慢日志统一阈值,默认1000毫秒(1秒):端到端批次、达梦事务、写入排队及非标异步写入。
|
||||||
syslog.slow-batch.threshold-ms=${SYSLOG_SLOW_BATCH_THRESHOLD_MS:500}
|
# 达到阈值时打印诊断;0关闭耗时触发的慢日志,但异常/失败仍记录。已有同名环境变量优先。
|
||||||
|
syslog.slow-batch.threshold-ms=${SYSLOG_SLOW_BATCH_THRESHOLD_MS:1000}
|
||||||
# 普通业务基础批量500(保留旧配置名);不再表示底层poll上限。已有同名环境变量仍覆盖基础值。
|
# 普通业务基础批量500(保留旧配置名);不再表示底层poll上限。已有同名环境变量仍覆盖基础值。
|
||||||
# 目标业务批量=ceil(基础条数*10000/max(普通保留万分比,非标保留万分比)),并受下面3000上限限制。
|
# 目标业务批量=ceil(基础条数*10000/max(普通保留万分比,非标保留万分比)),并受下面3000上限限制。
|
||||||
syslog.raw-listener.max-poll-records=${SYSLOG_RAW_LISTENER_MAX_POLL_RECORDS:500}
|
syslog.raw-listener.max-poll-records=${SYSLOG_RAW_LISTENER_MAX_POLL_RECORDS:500}
|
||||||
@@ -220,6 +221,12 @@ dm.standardized-write.batch-size=${DM_STANDARDIZED_WRITE_BATCH_SIZE:500}
|
|||||||
dm.standardized-write.writer-concurrency=2
|
dm.standardized-write.writer-concurrency=2
|
||||||
dm.standardized-write.writer-queue-capacity=100
|
dm.standardized-write.writer-queue-capacity=100
|
||||||
# 标准化大字段和非标原文均在达梦持久化边界截断;0 表示不限制。
|
# 标准化大字段和非标原文均在达梦持久化边界截断;0 表示不限制。
|
||||||
|
# 普通Kafka来源字段上限:默认2KB=2048个UTF-8字节(含截断标记),0关闭本层限制,启用时至少64。
|
||||||
|
# 在规则计算/采样之后限制普通表的文本字段(幂等ID/路由/分区等技术字段除外),告警不应用本层2KB限制,
|
||||||
|
# 以及非标表syslog_message/header_message/reason_detail;不影响InfluxDB原文,大日志不应用2KB限制。
|
||||||
|
# 重试/重放按originalTopic识别;改为4KB填4096。已有同名环境变量会覆盖JAR内默认值。
|
||||||
|
syslog.raw-listener.field-max-utf8-bytes=${SYSLOG_RAW_LISTENER_FIELD_MAX_UTF8_BYTES:2048}
|
||||||
|
# 以下旧限制继续生效(告警也保留原有大字段限制);同时命中时遵循较小上限,关闭上面的限制不会关闭以下限制。
|
||||||
dm.normal.large-field-max-utf8-bytes=${DM_NORMAL_LARGE_FIELD_MAX_UTF8_BYTES:262144}
|
dm.normal.large-field-max-utf8-bytes=${DM_NORMAL_LARGE_FIELD_MAX_UTF8_BYTES:262144}
|
||||||
dm.normal.large-fields=${DM_NORMAL_LARGE_FIELDS:http_req_body,http_resp_body,http_req_header,http_resp_header,detail,description,cmdline,shell_cmdline,parent_cmdline,mail_message}
|
dm.normal.large-fields=${DM_NORMAL_LARGE_FIELDS:http_req_body,http_resp_body,http_req_header,http_resp_header,detail,description,cmdline,shell_cmdline,parent_cmdline,mail_message}
|
||||||
dm.non-normal.large-field-max-utf8-bytes=${DM_NON_NORMAL_LARGE_FIELD_MAX_UTF8_BYTES:262144}
|
dm.non-normal.large-field-max-utf8-bytes=${DM_NON_NORMAL_LARGE_FIELD_MAX_UTF8_BYTES:262144}
|
||||||
|
|||||||
+49
@@ -65,6 +65,8 @@ class SysLogProcessorParallelTest {
|
|||||||
executor = Executors.newFixedThreadPool(4);
|
executor = Executors.newFixedThreadPool(4);
|
||||||
nonNormalExecutor = Executors.newSingleThreadExecutor();
|
nonNormalExecutor = Executors.newSingleThreadExecutor();
|
||||||
processor = new SysLogProcessor();
|
processor = new SysLogProcessor();
|
||||||
|
ReflectionTestUtils.setField(processor, "rawLaneFieldLimiter",
|
||||||
|
new com.common.service.RawLaneFieldLimiter(2048, "raw"));
|
||||||
normalProcessor = mock(LogNormalProcessor.class);
|
normalProcessor = mock(LogNormalProcessor.class);
|
||||||
failureRouter = mock(SyslogFailureRouter.class);
|
failureRouter = mock(SyslogFailureRouter.class);
|
||||||
batchWriter = mock(DmStandardizedBatchWriter.class);
|
batchWriter = mock(DmStandardizedBatchWriter.class);
|
||||||
@@ -459,6 +461,53 @@ class SysLogProcessorParallelTest {
|
|||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void rawOutputIsLimitedAfterRulesAndLargeOutputIsUnchangedIncludingRetry() {
|
||||||
|
for (String originalTopic : java.util.Arrays.asList("raw", "raw.large")) {
|
||||||
|
ConsumerRecord<String, String> record = new ConsumerRecord<>("retry", 0, 0L, "key", "ciphertext");
|
||||||
|
SyslogFailureRouter.MessageContext context = new SyslogFailureRouter.MessageContext(
|
||||||
|
"ciphertext", "stable-id", 1, null, Instant.EPOCH, originalTopic, null);
|
||||||
|
String content = String.join("", Collections.nCopies(3000, "中"));
|
||||||
|
SysLogProcessor.PreparedMessage source = new SysLogProcessor.PreparedMessage(record, context,
|
||||||
|
content, "stable-id", null, LocalDateTime.now(), 7L, 3L, System.currentTimeMillis());
|
||||||
|
java.util.Map<String, Object> normal = normalizedRow();
|
||||||
|
normal.put("custom_field", content);
|
||||||
|
java.util.Map<String, Object> alarm = normalizedRow();
|
||||||
|
alarm.put("event_level", "HIGH");
|
||||||
|
alarm.put("description", content);
|
||||||
|
SyslogNonNormalMessage nonNormal = new SyslogNonNormalMessage();
|
||||||
|
nonNormal.setSyslogMessage(content);
|
||||||
|
List<SysLogProcessor.PreparedNormalization> computed = java.util.Arrays.asList(
|
||||||
|
new SysLogProcessor.PreparedNormalization(source,
|
||||||
|
LogNormalProcessor.NormalizationResult.success(java.util.Arrays.asList(normal, alarm)), null),
|
||||||
|
new SysLogProcessor.PreparedNormalization(source,
|
||||||
|
LogNormalProcessor.NormalizationResult.filtered(Collections.singletonList(nonNormal)), null));
|
||||||
|
Object retainedPlan = ReflectionTestUtils.invokeMethod(processor, "allRetainedPlan", computed);
|
||||||
|
Object persistencePlan = ReflectionTestUtils.invokeMethod(processor, "prepareStandardizedPersistence",
|
||||||
|
ReflectionTestUtils.getField(retainedPlan, "normalizations"), new SysLogProcessor.BatchMetrics());
|
||||||
|
for (String requestsName : java.util.Arrays.asList("normalWriteRequests", "alarmWriteRequests")) {
|
||||||
|
List<DmStandardizedBatchWriter.WriteRequest<SysLogProcessor.PreparedMessage>> requests =
|
||||||
|
(List<DmStandardizedBatchWriter.WriteRequest<SysLogProcessor.PreparedMessage>>)
|
||||||
|
ReflectionTestUtils.getField(persistencePlan, requestsName);
|
||||||
|
assertEquals(1, requests.size(), "告警和普通记录数量不变");
|
||||||
|
String stored = (String) requests.get(0).getData().get(
|
||||||
|
"alarmWriteRequests".equals(requestsName) ? "description" : "custom_field");
|
||||||
|
if ("raw".equals(originalTopic) && "normalWriteRequests".equals(requestsName)) {
|
||||||
|
assertTrue(stored.getBytes(java.nio.charset.StandardCharsets.UTF_8).length <= 2048);
|
||||||
|
assertTrue(stored.contains("HB_XDR_TRUNCATED"));
|
||||||
|
} else {
|
||||||
|
assertEquals(content, stored);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals(content, normal.get("custom_field"), "规则计算结果不被原地覆盖");
|
||||||
|
assertEquals(content, alarm.get("description"));
|
||||||
|
assertEquals(content, ReflectionTestUtils.getField(source, "decryptedMessage"), "Influx原文不改变");
|
||||||
|
if ("raw".equals(originalTopic)) assertTrue(nonNormal.getSyslogMessage().contains("HB_XDR_TRUNCATED"));
|
||||||
|
else assertEquals(content, nonNormal.getSyslogMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<SysLogProcessor.PreparedMessage> oldMessages(int count) {
|
private List<SysLogProcessor.PreparedMessage> oldMessages(int count) {
|
||||||
List<SysLogProcessor.PreparedMessage> old = new ArrayList<>();
|
List<SysLogProcessor.PreparedMessage> old = new ArrayList<>();
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
|
|||||||
+12
-1
@@ -113,13 +113,24 @@ class DmPreparedBatchInserterTest {
|
|||||||
|
|
||||||
DmPreparedBatchInserter inserter = new DmPreparedBatchInserter(mock(
|
DmPreparedBatchInserter inserter = new DmPreparedBatchInserter(mock(
|
||||||
org.springframework.jdbc.core.JdbcTemplate.class));
|
org.springframework.jdbc.core.JdbcTemplate.class));
|
||||||
|
DmWriteSizeEstimate size = new DmWriteSizeEstimate();
|
||||||
DmPreparedBatchInserter.BatchExecution result = inserter.execute(connection,
|
DmPreparedBatchInserter.BatchExecution result = inserter.execute(connection,
|
||||||
DmPreparedBatchInserter.NORMAL_TABLE, Arrays.asList(first, second), 45);
|
DmPreparedBatchInserter.NORMAL_TABLE, Arrays.asList(first, second), 45, size);
|
||||||
|
|
||||||
assertEquals(2, result.getRows());
|
assertEquals(2, result.getRows());
|
||||||
assertEquals(2, result.getColumns());
|
assertEquals(2, result.getColumns());
|
||||||
|
assertEquals(18L, size.getPayloadBytes());
|
||||||
|
assertEquals(13L, size.getMaxRowBytes());
|
||||||
|
assertEquals("src_ip", size.getMaxFieldName());
|
||||||
verify(insertStatement, times(2)).addBatch();
|
verify(insertStatement, times(2)).addBatch();
|
||||||
verify(insertStatement).executeBatch();
|
verify(insertStatement).executeBatch();
|
||||||
verify(insertStatement).setNull(2, Types.VARCHAR);
|
verify(insertStatement).setNull(2, Types.VARCHAR);
|
||||||
|
|
||||||
|
when(insertStatement.executeBatch()).thenThrow(new java.sql.SQLException("test failure"));
|
||||||
|
DmWriteSizeEstimate failedSize = new DmWriteSizeEstimate();
|
||||||
|
assertThrows(java.sql.SQLException.class, () -> inserter.execute(connection,
|
||||||
|
DmPreparedBatchInserter.NORMAL_TABLE, Arrays.asList(first, second), 45, failedSize));
|
||||||
|
assertEquals("COMPLETE", failedSize.getStatus());
|
||||||
|
assertEquals(18L, failedSize.getPayloadBytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
@@ -54,6 +54,34 @@ class DmStandardizedBatchWriterTest {
|
|||||||
when(normalDataService.isAlarmData(any())).thenReturn(false);
|
when(normalDataService.isAlarmData(any())).thenReturn(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void alarmsKeepLegacyTruncationInBothBatchAndSingleMode() {
|
||||||
|
DmLargeTextLimiter limiter = new DmLargeTextLimiter();
|
||||||
|
ReflectionTestUtils.setField(limiter, "normalMaxBytes", 64L);
|
||||||
|
ReflectionTestUtils.setField(limiter, "normalFieldsValue", "description");
|
||||||
|
limiter.validate();
|
||||||
|
ReflectionTestUtils.setField(writer, "largeTextLimiter", limiter);
|
||||||
|
when(normalDataService.isAlarmData(any())).thenAnswer(invocation ->
|
||||||
|
((Map<?, ?>) invocation.getArgument(0)).get("event_level") != null);
|
||||||
|
when(normalDataService.insertDynamic(any())).thenReturn(1);
|
||||||
|
String content = String.join("", java.util.Collections.nCopies(300000, "x"));
|
||||||
|
for (String mode : java.util.Arrays.asList("BATCH", "SINGLE")) {
|
||||||
|
ReflectionTestUtils.setField(writer, "mode", mode);
|
||||||
|
List<DmStandardizedBatchWriter.WriteRequest<String>> requests = requests(2, -1);
|
||||||
|
requests.get(0).getData().put("event_level", "HIGH");
|
||||||
|
requests.get(0).getData().put("description", content);
|
||||||
|
requests.get(1).getData().put("description", content);
|
||||||
|
DmStandardizedBatchWriter.WriteReport<String> report = writer.write(requests);
|
||||||
|
assertEquals(2, report.getSuccessfulRows());
|
||||||
|
String alarm = (String) requests.get(0).getData().get("description");
|
||||||
|
assertTrue(alarm.contains("HB_XDR_TRUNCATED"), "告警仍遵守旧的大字段上限");
|
||||||
|
assertTrue(alarm.getBytes(java.nio.charset.StandardCharsets.UTF_8).length <= 64);
|
||||||
|
String ordinary = (String) requests.get(1).getData().get("description");
|
||||||
|
assertTrue(ordinary.contains("HB_XDR_TRUNCATED"));
|
||||||
|
assertTrue(ordinary.getBytes(java.nio.charset.StandardCharsets.UTF_8).length <= 64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void oneBadRowIsIsolatedWithoutDowngradingEveryGoodRowToSingleInsert() {
|
void oneBadRowIsIsolatedWithoutDowngradingEveryGoodRowToSingleInsert() {
|
||||||
List<DmStandardizedBatchWriter.WriteRequest<String>> requests = requests(100, 73);
|
List<DmStandardizedBatchWriter.WriteRequest<String>> requests = requests(100, 73);
|
||||||
|
|||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
package com.common.service;
|
||||||
|
|
||||||
|
import ch.qos.logback.classic.Logger;
|
||||||
|
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||||
|
import ch.qos.logback.core.read.ListAppender;
|
||||||
|
import org.apache.ibatis.session.Configuration;
|
||||||
|
import org.apache.ibatis.session.SqlSessionFactory;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
import org.springframework.transaction.TransactionDefinition;
|
||||||
|
import org.springframework.transaction.support.AbstractPlatformTransactionManager;
|
||||||
|
import org.springframework.transaction.support.DefaultTransactionStatus;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
class DmTransactionWriteSizeLogTest {
|
||||||
|
@Test
|
||||||
|
void successfulSlowNormalAndAlarmTransactionsIncludeOnlySizeMetadata() {
|
||||||
|
check(false, false);
|
||||||
|
check(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void failedWriteIncludesAttemptedPayloadAndStillThrows() {
|
||||||
|
check(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void check(boolean alarm, boolean fail) {
|
||||||
|
SqlSessionFactory sessions = mock(SqlSessionFactory.class);
|
||||||
|
Configuration configuration = new Configuration();
|
||||||
|
configuration.setEnvironment(new org.apache.ibatis.mapping.Environment("test",
|
||||||
|
new org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory(), mock(javax.sql.DataSource.class)));
|
||||||
|
when(sessions.getConfiguration()).thenReturn(configuration);
|
||||||
|
DmPreparedBatchInserter inserter = mock(DmPreparedBatchInserter.class);
|
||||||
|
when(inserter.insert(eq(alarm), anyList(), anyInt(), any(DmWriteSizeEstimate.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
DmWriteSizeEstimate size = invocation.getArgument(3);
|
||||||
|
size.measure(invocation.getArgument(1), DmWriteSizeEstimateTest.columns("detail"));
|
||||||
|
if (fail) throw new IllegalStateException("write failed");
|
||||||
|
return new DmPreparedBatchInserter.BatchExecution(1, 1, 0, 0, 0);
|
||||||
|
});
|
||||||
|
AbstractPlatformTransactionManager transactions = new AbstractPlatformTransactionManager() {
|
||||||
|
protected Object doGetTransaction() { return new Object(); }
|
||||||
|
protected void doBegin(Object transaction, TransactionDefinition definition) { }
|
||||||
|
protected void doCommit(DefaultTransactionStatus status) {
|
||||||
|
// 使成功事务确定超过1ms阈值,不依赖真实数据库。
|
||||||
|
java.util.concurrent.locks.LockSupport.parkNanos(5_000_000L);
|
||||||
|
}
|
||||||
|
protected void doRollback(DefaultTransactionStatus status) { }
|
||||||
|
};
|
||||||
|
DmStandardizedBatchExecutor executor = new DmStandardizedBatchExecutor(sessions, transactions,
|
||||||
|
inserter, mock(StatsChangeCaptureService.class), mock(DirectStatsProjectionWriter.class),
|
||||||
|
new DmDirectTransactionMetrics(), mock(LateAlarmBucketWriter.class));
|
||||||
|
ReflectionTestUtils.setField(executor, "slowTransactionThresholdMs", 1L);
|
||||||
|
Logger logger = (Logger) LoggerFactory.getLogger(DmStandardizedBatchExecutor.class);
|
||||||
|
ListAppender<ILoggingEvent> appender = new ListAppender<>();
|
||||||
|
appender.start();
|
||||||
|
logger.addAppender(appender);
|
||||||
|
List<Map<String, Object>> rows = Collections.singletonList(
|
||||||
|
Collections.<String, Object>singletonMap("detail", "秘密😀"));
|
||||||
|
try {
|
||||||
|
if (fail) assertThrows(IllegalStateException.class, () -> executor.execute(alarm, rows, 45));
|
||||||
|
else executor.execute(alarm, rows, 45);
|
||||||
|
String message = appender.list.stream().map(ILoggingEvent::getFormattedMessage)
|
||||||
|
.filter(text -> text.contains("[SLOW_DM_TRANSACTION]")).findFirst().orElse("");
|
||||||
|
assertTrue(message.contains("table=" + (alarm ? "syslog_normal_alarm" : "syslog_normal_data")));
|
||||||
|
assertTrue(message.contains("status=" + (fail ? "FAILED" : "SUCCESS")));
|
||||||
|
assertTrue(message.contains("sizeEstimateStatus=COMPLETE"));
|
||||||
|
assertTrue(message.contains("estimatedPayloadBytes=10"));
|
||||||
|
assertTrue(message.contains("maxRowEstimatedBytes=10"));
|
||||||
|
assertTrue(message.contains("maxFieldEstimatedBytes=10"));
|
||||||
|
assertTrue(message.contains("maxFieldName=detail"));
|
||||||
|
assertTrue(message.contains("sizeEstimateMs="));
|
||||||
|
assertFalse(message.contains("秘密"));
|
||||||
|
} finally {
|
||||||
|
logger.detachAppender(appender);
|
||||||
|
appender.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
package com.common.service;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.sql.Types;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class DmWriteSizeEstimateTest {
|
||||||
|
@Test
|
||||||
|
void utf8MatchesJdkForAsciiChineseEmojiMalformedSurrogatesAndRandomText() {
|
||||||
|
for (String text : Arrays.asList("", "ascii", "中文😀", "\uD800", "\uDC00", "\uD800x\uDC00")) {
|
||||||
|
assertEquals(text.getBytes(StandardCharsets.UTF_8).length, DmWriteSizeEstimate.utf8Length(text));
|
||||||
|
}
|
||||||
|
Random random = new Random(42);
|
||||||
|
for (int i = 0; i < 200; i++) {
|
||||||
|
char[] chars = new char[1024];
|
||||||
|
for (int j = 0; j < chars.length; j++) chars[j] = (char) random.nextInt(65536);
|
||||||
|
String text = new String(chars);
|
||||||
|
assertEquals(text.getBytes(StandardCharsets.UTF_8).length, DmWriteSizeEstimate.utf8Length(text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void measuresOnlyBoundColumnsAndDoesNotMutateRowsOrLogValues() {
|
||||||
|
Map<String, Object> first = new LinkedHashMap<>();
|
||||||
|
first.put("id", "a");
|
||||||
|
first.put("detail", "敏感内容😀"); // 16 UTF-8 bytes
|
||||||
|
first.put("ignored", "not-in-sql");
|
||||||
|
Map<String, Object> second = new LinkedHashMap<>();
|
||||||
|
second.put("id", "bc");
|
||||||
|
second.put("detail", new byte[3]);
|
||||||
|
Map<String, Object> snapshot = new LinkedHashMap<>(first);
|
||||||
|
DmWriteSizeEstimate size = new DmWriteSizeEstimate();
|
||||||
|
size.measure(Arrays.asList(first, second), columns("id", "detail"));
|
||||||
|
assertEquals(22L, size.getPayloadBytes());
|
||||||
|
assertEquals(17L, size.getMaxRowBytes());
|
||||||
|
assertEquals(16L, size.getMaxFieldBytes());
|
||||||
|
assertEquals("detail", size.getMaxFieldName());
|
||||||
|
assertEquals("COMPLETE", size.getStatus());
|
||||||
|
assertEquals(snapshot, first);
|
||||||
|
assertFalse(size.toString().contains("敏感内容"));
|
||||||
|
assertFalse(size.toString().contains("not-in-sql"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void measuresTruncatedValuesNotOriginalAndKeepsTruncationMarker() {
|
||||||
|
DmLargeTextLimiter limiter = new DmLargeTextLimiter();
|
||||||
|
ReflectionTestUtils.setField(limiter, "normalMaxBytes", 64L);
|
||||||
|
ReflectionTestUtils.setField(limiter, "normalFieldsValue", "detail");
|
||||||
|
limiter.validate();
|
||||||
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("detail", String.join("", Collections.nCopies(100, "中")));
|
||||||
|
limiter.truncateNormalRow(row);
|
||||||
|
String truncated = (String) row.get("detail");
|
||||||
|
assertTrue(truncated.getBytes(StandardCharsets.UTF_8).length <= 64);
|
||||||
|
DmWriteSizeEstimate size = new DmWriteSizeEstimate();
|
||||||
|
size.measure(Collections.singletonList(row), columns("detail"));
|
||||||
|
assertEquals(truncated.getBytes(StandardCharsets.UTF_8).length, size.getPayloadBytes());
|
||||||
|
assertSame(truncated, row.get("detail"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void handlesNullScalarsAndUnknownWithoutCallingArbitraryToString() {
|
||||||
|
Object unknown = new Object() {
|
||||||
|
@Override public String toString() { throw new AssertionError("must not stringify"); }
|
||||||
|
};
|
||||||
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("id", 123);
|
||||||
|
row.put("flag", true);
|
||||||
|
row.put("unknown", unknown);
|
||||||
|
row.put("missing", null);
|
||||||
|
DmWriteSizeEstimate size = new DmWriteSizeEstimate();
|
||||||
|
size.measure(Collections.singletonList(row), columns("id", "flag", "unknown", "missing"));
|
||||||
|
assertEquals(7L, size.getPayloadBytes());
|
||||||
|
assertEquals(1L, size.getUnmeasuredValues());
|
||||||
|
assertEquals("PARTIAL", size.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void diagnosticFailureIsMarkedWithoutThrowingAndUnmeasuredIsExplicit() {
|
||||||
|
DmWriteSizeEstimate size = new DmWriteSizeEstimate();
|
||||||
|
assertEquals("NOT_MEASURED", size.getStatus());
|
||||||
|
Map<String, Object> bad = new HashMap<String, Object>() {
|
||||||
|
@Override public Object get(Object key) { throw new IllegalStateException("sensitive value"); }
|
||||||
|
};
|
||||||
|
assertDoesNotThrow(() -> size.measure(Collections.singletonList(bad), columns("id")));
|
||||||
|
assertEquals("FAILED", size.getStatus());
|
||||||
|
assertFalse(size.toString().contains("sensitive value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void totalUsesLongBeyondTwoGiBWithoutAllocatingAnEncodedCopy() {
|
||||||
|
String text = String.join("", Collections.nCopies(1024 * 1024, "a"));
|
||||||
|
DmWriteSizeEstimate size = new DmWriteSizeEstimate();
|
||||||
|
size.measure(Collections.nCopies(2050, Collections.<String, Object>singletonMap("detail", text)),
|
||||||
|
columns("detail"));
|
||||||
|
assertEquals(2050L * 1024 * 1024, size.getPayloadBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<DmPreparedBatchInserter.ColumnMetadata> columns(String... names) {
|
||||||
|
List<DmPreparedBatchInserter.ColumnMetadata> columns = new ArrayList<>();
|
||||||
|
for (String name : names) columns.add(new DmPreparedBatchInserter.ColumnMetadata(name, Types.VARCHAR));
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
}
|
||||||
+112
@@ -0,0 +1,112 @@
|
|||||||
|
package com.common.service;
|
||||||
|
|
||||||
|
import com.common.entity.SyslogNonNormalMessage;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||||
|
import org.springframework.core.env.MapPropertySource;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class RawLaneFieldLimiterTest {
|
||||||
|
@Test
|
||||||
|
void springDefaultIsTwoKiBAndExplicitOverrideWorks() {
|
||||||
|
checkSpringLimit(null, 2048);
|
||||||
|
checkSpringLimit("4096", 4096);
|
||||||
|
checkSpringLimit("0", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkSpringLimit(String override, int expected) {
|
||||||
|
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
|
||||||
|
Map<String, Object> properties = new HashMap<>();
|
||||||
|
properties.put("spring.kafka.consumer.topic", "raw");
|
||||||
|
if (override != null) properties.put("syslog.raw-listener.field-max-utf8-bytes", override);
|
||||||
|
context.getEnvironment().getPropertySources().addFirst(new MapPropertySource("test", properties));
|
||||||
|
context.register(RawLaneFieldLimiter.class);
|
||||||
|
context.refresh();
|
||||||
|
RawLaneFieldLimiter limiter = context.getBean(RawLaneFieldLimiter.class);
|
||||||
|
Map<String, Object> row = new HashMap<>();
|
||||||
|
String content = repeat("a", 5000);
|
||||||
|
row.put("custom_field", content);
|
||||||
|
limiter.truncateRow(row);
|
||||||
|
assertEquals(expected == 0 ? content : expected,
|
||||||
|
expected == 0 ? row.get("custom_field") : bytes(row.get("custom_field")));
|
||||||
|
assertEquals(expected != 0, limiter.appliesTo("raw", null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void truncatesCustomTextByUtf8WithMarkerAndPreservesTypesAndIdentity() {
|
||||||
|
RawLaneFieldLimiter limiter = new RawLaneFieldLimiter(2048, "raw");
|
||||||
|
Map<String, Object> row = new HashMap<>();
|
||||||
|
String content = repeat("中文🙂", 1000);
|
||||||
|
row.put("custom_field", content);
|
||||||
|
row.put("id", content);
|
||||||
|
row.put("event_level", "HIGH");
|
||||||
|
row.put("number", 123L);
|
||||||
|
row.put("nothing", null);
|
||||||
|
byte[] binary = new byte[3000];
|
||||||
|
row.put("binary", binary);
|
||||||
|
row.put("exact", repeat("a", 2048));
|
||||||
|
row.put("short", "短字段");
|
||||||
|
limiter.truncateRow(row);
|
||||||
|
String stored = (String) row.get("custom_field");
|
||||||
|
assertTrue(bytes(stored) <= 2048);
|
||||||
|
assertTrue(stored.endsWith("...[HB_XDR_TRUNCATED original_utf8_bytes=10000]"));
|
||||||
|
assertEquals(stored, new String(stored.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8));
|
||||||
|
assertEquals(content, row.get("id"));
|
||||||
|
assertEquals("HIGH", row.get("event_level"));
|
||||||
|
assertEquals(123L, row.get("number"));
|
||||||
|
assertNull(row.get("nothing"));
|
||||||
|
assertSame(binary, row.get("binary"));
|
||||||
|
assertEquals(repeat("a", 2048), row.get("exact"));
|
||||||
|
assertEquals("短字段", row.get("short"));
|
||||||
|
limiter.truncateRow(row);
|
||||||
|
assertEquals(stored, row.get("custom_field"), "重复处理不能重复追加标记");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void onlyOriginalRawTopicAppliesIncludingRetryAndReplay() {
|
||||||
|
RawLaneFieldLimiter limiter = new RawLaneFieldLimiter(2048, "custom-raw");
|
||||||
|
assertTrue(limiter.appliesTo("custom-raw", null));
|
||||||
|
assertTrue(limiter.appliesTo("retry", "custom-raw"));
|
||||||
|
assertTrue(limiter.appliesTo("replay", "custom-raw"));
|
||||||
|
assertFalse(limiter.appliesTo("custom-large", null));
|
||||||
|
assertFalse(limiter.appliesTo("retry", "custom-large"));
|
||||||
|
assertFalse(limiter.appliesTo("custom-raw", "custom-large"));
|
||||||
|
assertFalse(limiter.appliesTo("retry", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nonNormalContentLimitedButIdentityUnchanged() {
|
||||||
|
RawLaneFieldLimiter limiter = new RawLaneFieldLimiter(2048, "raw");
|
||||||
|
SyslogNonNormalMessage message = new SyslogNonNormalMessage();
|
||||||
|
String content = repeat("x", 4096);
|
||||||
|
message.setSyslogMessage(content);
|
||||||
|
message.setHeaderMessage(content);
|
||||||
|
message.setReasonDetail(content);
|
||||||
|
message.setSyslogUuid("stable-id");
|
||||||
|
limiter.truncateNonNormal(message);
|
||||||
|
assertEquals(2048, bytes(message.getSyslogMessage()));
|
||||||
|
assertEquals(2048, bytes(message.getHeaderMessage()));
|
||||||
|
assertEquals(2048, bytes(message.getReasonDetail()));
|
||||||
|
assertEquals("stable-id", message.getSyslogUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validatesMarkerBudget() {
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> new RawLaneFieldLimiter(-1, "raw"));
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> new RawLaneFieldLimiter(63, "raw"));
|
||||||
|
assertDoesNotThrow(() -> new RawLaneFieldLimiter(64, "raw"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int bytes(Object text) { return ((String) text).getBytes(StandardCharsets.UTF_8).length; }
|
||||||
|
private static String repeat(String text, int count) {
|
||||||
|
StringBuilder value = new StringBuilder();
|
||||||
|
for (int i = 0; i < count; i++) value.append(text);
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
@@ -15,6 +15,7 @@ class SyslogBuildStartupLoggerTest {
|
|||||||
MockEnvironment environment = new MockEnvironment()
|
MockEnvironment environment = new MockEnvironment()
|
||||||
.withProperty("syslog.raw-listener.max-poll-records", "700")
|
.withProperty("syslog.raw-listener.max-poll-records", "700")
|
||||||
.withProperty("syslog.raw-listener.adaptive-max-records", "2500")
|
.withProperty("syslog.raw-listener.adaptive-max-records", "2500")
|
||||||
|
.withProperty("syslog.slow-batch.threshold-ms", "1500")
|
||||||
.withProperty("stats.projection-async.enabled", "false");
|
.withProperty("stats.projection-async.enabled", "false");
|
||||||
environment.setActiveProfiles("prod");
|
environment.setActiveProfiles("prod");
|
||||||
SyslogBuildStartupLogger reporter = new SyslogBuildStartupLogger(environment);
|
SyslogBuildStartupLogger reporter = new SyslogBuildStartupLogger(environment);
|
||||||
@@ -32,6 +33,7 @@ class SyslogBuildStartupLoggerTest {
|
|||||||
assertTrue(message.contains("baseProcessingRecords=700"));
|
assertTrue(message.contains("baseProcessingRecords=700"));
|
||||||
assertTrue(message.contains("adaptiveMaxRecords=2500"));
|
assertTrue(message.contains("adaptiveMaxRecords=2500"));
|
||||||
assertTrue(message.contains("statsPublisherEnabled=false"));
|
assertTrue(message.contains("statsPublisherEnabled=false"));
|
||||||
|
assertTrue(message.contains("slowBatchThresholdMs=1500"));
|
||||||
} finally {
|
} finally {
|
||||||
logger.detachAppender(appender);
|
logger.detachAppender(appender);
|
||||||
appender.stop();
|
appender.stop();
|
||||||
@@ -46,6 +48,8 @@ class SyslogBuildStartupLoggerTest {
|
|||||||
assertFalse(summary.contains("buildTime="));
|
assertFalse(summary.contains("buildTime="));
|
||||||
assertTrue(summary.contains("baseProcessingRecords=500"));
|
assertTrue(summary.contains("baseProcessingRecords=500"));
|
||||||
assertTrue(summary.contains("adaptiveMaxRecords=3000"));
|
assertTrue(summary.contains("adaptiveMaxRecords=3000"));
|
||||||
|
assertTrue(summary.contains("slowBatchThresholdMs=1000"));
|
||||||
|
assertTrue(summary.contains("rawFieldMaxUtf8Bytes=2048"));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,12 +125,43 @@ docker exec syslog-consumer-rule-dm sh -c 'curl -fsS http://127.0.0.1:8289/xdrru
|
|||||||
`dropped_fact_rows_total` 指受统计缺失影响的事实行数/尝试行数,并不表示事实数据被删除。
|
`dropped_fact_rows_total` 指受统计缺失影响的事实行数/尝试行数,并不表示事实数据被删除。
|
||||||
这些内存指标会在重启后清零,不能覆盖崩溃时丢失的统计。损失日志最多每10秒一次,计数逐次累加。
|
这些内存指标会在重启后清零,不能覆盖崩溃时丢失的统计。损失日志最多每10秒一次,计数逐次累加。
|
||||||
`published_total` 仅表示Kafka确认,不代表统计已落达梦;统计最终入库还需检查投影消费组积压。
|
`published_total` 仅表示Kafka确认,不代表统计已落达梦;统计最终入库还需检查投影消费组积压。
|
||||||
`SLOW_DM_TRANSACTION` 自动沿用500ms阈值,无须打开所有事务INFO日志:
|
`SLOW_DM_TRANSACTION` 自动沿用1000ms(1秒)默认阈值,无须打开所有事务INFO日志:
|
||||||
`connectionBeginMs`包括取连接/事务初始化,`lateAlarmBucketMs`仅为分钟桶小键准备/注册提交回调,
|
`connectionBeginMs`包括取连接/事务初始化,`lateAlarmBucketMs`仅为分钟桶小键准备/注册提交回调,
|
||||||
`jdbcExecuteMs`是JDBC批执行,`statsProjectionStageMs`在DIRECT下是统计事件构造/注册回调阶段,
|
`jdbcExecuteMs`是JDBC批执行,`statsProjectionStageMs`在DIRECT下是统计事件构造/注册回调阶段,
|
||||||
不含后台Kafka/统计写库;`commitMs`是事实事务提交阶段。状态中的统计成功指标表示聚合/注册完成,不保证投影落库。
|
不含后台Kafka/统计写库;`commitMs`是事实事务提交阶段。状态中的统计成功指标表示聚合/注册完成,不保证投影落库。
|
||||||
`SLOW_DM_QUEUE` 单独说明批写任务排队超过阈值。
|
`SLOW_DM_QUEUE` 单独说明批写任务排队超过阈值。
|
||||||
|
|
||||||
|
### 达梦写入内容大小诊断
|
||||||
|
|
||||||
|
`SLOW_DM_TRANSACTION` 的普通表/告警表成功慢事务、失败事务日志增加以下字段,无需新配置、数据库或Kafka脚本:
|
||||||
|
|
||||||
|
| 字段 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| sizeEstimateStatus | COMPLETE:支持的参数内容已测量;PARTIAL:有未测量类型;FAILED:估算异常;NOT_MEASURED:尚未执行到测量阶段 |
|
||||||
|
| estimatedPayloadBytes | 本次小事务实际INSERT列的非空参数内容估算总字节数 |
|
||||||
|
| maxRowEstimatedBytes | 单行内容估算最大字节数 |
|
||||||
|
| maxFieldEstimatedBytes / maxFieldName | 单个字段值最大估算字节数及列名,不打印内容;同大小取首个,均为零时名称为none |
|
||||||
|
| unmeasuredValues | 不支持测量的非空参数个数,如LOB/流/自定义对象;不会为了测量读取它们 |
|
||||||
|
| sizeEstimateMs | 大小估算自身耗时,包含在事务总耗时中,不计入jdbcExecuteMs或commitMs |
|
||||||
|
|
||||||
|
测量在已有字段截断和字段准备之后、按表元数据确定INSERT列之后、绑定和执行之前进行。
|
||||||
|
String按UTF-8计数(含中文、emoji,不创建完整编码字节数组),byte[]按原始长度计数,
|
||||||
|
常用数值/布尔/日期按文本表示估算,NULL计零。不包含SQL文本、驱动协议、索引、数据库页及REDO开销,
|
||||||
|
也不等于数据库字符集编码后的物理长度。COMPLETE表示参数内容估算完整,不代表精确磁盘字节数。
|
||||||
|
失败事务表示本次尝试量,不是成功落库量;失败/二分/重试的多条日志不能累加为净新增量。
|
||||||
|
NOT_MEASURED或FAILED中的零/部分值不能理解为本次写入内容为空。
|
||||||
|
仅增加观测,不修改字段值、截断阈值、保存条数或Kafka确认策略;不覆盖现有非标异步写库路径。
|
||||||
|
若需要快慢批次对照,已有stats.direct.transaction-log-enabled=true可打印DIRECT成功事务信息(日志量会增加)。
|
||||||
|
|
||||||
|
客户仍在宿主机执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs --since 10m syslog-consumer-rule-dm 2>&1 | grep 'SLOW_DM_TRANSACTION' | tail -30
|
||||||
|
```
|
||||||
|
|
||||||
|
将estimatedPayloadBytes、maxRowEstimatedBytes与jdbcExecuteMs、commitMs一起比较,
|
||||||
|
通过batchId关联SLOW_SYSLOG_BATCH;不能将polledMessageBytes视为达梦写入大小。
|
||||||
|
|
||||||
在有Kafka脚本的现场节点执行(已确认bootstrap地址):
|
在有Kafka脚本的现场节点执行(已确认bootstrap地址):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -182,7 +213,12 @@ docker exec syslog-consumer-rule-dm sh -c 'curl -fsS http://127.0.0.1:8289/xdrru
|
|||||||
|
|
||||||
`stageMs`各阶段为互斥墙钟时间,跨业务块累加;旧有normalizeMs等字段包含部分子阶段,不能再与stageMs相加。
|
`stageMs`各阶段为互斥墙钟时间,跨业务块累加;旧有normalizeMs等字段包含部分子阶段,不能再与stageMs相加。
|
||||||
兼容字段ruleComputeWorkerMs实际是整批规则计算墙钟耗时;新增ruleWorkerBusySumMs才是worker耗时之和,二者不要混淆。
|
兼容字段ruleComputeWorkerMs实际是整批规则计算墙钟耗时;新增ruleWorkerBusySumMs才是worker耗时之和,二者不要混淆。
|
||||||
业务异常和提交异常即使小于500ms也输出诊断。`processedRecords/prepared`等旧计数仅覆盖完成的业务块;
|
慢日志统一使用 `syslog.slow-batch.threshold-ms`,默认1000ms,适用于 `SLOW_SYSLOG_BATCH`、
|
||||||
|
`SLOW_DM_TRANSACTION`、`SLOW_DM_QUEUE` 和 `SLOW_NON_NORMAL_WRITE`。
|
||||||
|
如容器已有 `SYSLOG_SLOW_BATCH_THRESHOLD_MS=500`,需删除该覆盖或改为1000;仅替换JAR不会覆盖环境变量。
|
||||||
|
启动日志 `SYSLOG_BUILD_READY` 中的 `slowBatchThresholdMs` 可确认生效值。无需执行数据库或Kafka脚本。
|
||||||
|
|
||||||
|
业务异常和提交异常即使小于1000ms也输出诊断。`processedRecords/prepared`等旧计数仅覆盖完成的业务块;
|
||||||
出现failedChunks时不能把它们理解为整次poll已成功入库。无限等待中的操作尚未结束,不会提前输出最终慢批次。
|
出现failedChunks时不能把它们理解为整次poll已成功入库。无限等待中的操作尚未结束,不会提前输出最终慢批次。
|
||||||
|
|
||||||
采样审计接口新增 `audit_async`,直接统计接口的 `dm_transaction.late_alarm_bucket_async` 查看通知队列。
|
采样审计接口新增 `audit_async`,直接统计接口的 `dm_transaction.late_alarm_bucket_async` 查看通知队列。
|
||||||
@@ -192,6 +228,30 @@ docker exec syslog-consumer-rule-dm sh -c 'curl -fsS http://127.0.0.1:8289/xdrru
|
|||||||
新审计指标 `audit_lost_or_unconfirmed_messages_total` 表示未记录或提交结果未知的消息数,
|
新审计指标 `audit_lost_or_unconfirmed_messages_total` 表示未记录或提交结果未知的消息数,
|
||||||
旧 `audit_fallback_messages_total` 本路径不再增加。
|
旧 `audit_fallback_messages_total` 本路径不再增加。
|
||||||
|
|
||||||
|
## 普通通道字段截断(默认2KB)
|
||||||
|
|
||||||
|
配置文件:`syslog-consumer-rule/src/main/resources/application.properties`(prod也继承)。
|
||||||
|
|
||||||
|
```properties
|
||||||
|
syslog.raw-listener.field-max-utf8-bytes=${SYSLOG_RAW_LISTENER_FIELD_MAX_UTF8_BYTES:2048}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 默认2048个UTF-8字节,不是2048个汉字;改为4KB时填4096。0仅关闭本层限制,启用时至少64。
|
||||||
|
- 对原始Topic等于 `spring.kafka.consumer.topic` 的消息生效。Retry/Replay优先按信封的originalTopic判断;
|
||||||
|
缺失originalTopic时只按当前Topic判断,不猜测大日志来源。
|
||||||
|
- 在规则计算及采样决策后、达梦写入前处理:`syslog_normal_data` 的文本内容字段
|
||||||
|
(包括自定义映射字段),以及非标表的 `syslog_message`、`header_message`、`reason_detail`。
|
||||||
|
- 不截断数值、日期、二进制对象;标准化结果的ID、syslog_uuid、Topic、规则版本、设备/租户/采集ID、
|
||||||
|
分区时间和event_level等技术字段保持不变。`syslog_normal_alarm` 跳过新增2KB限制,
|
||||||
|
但仍保留原有 `dm.normal.large-field-max-utf8-bytes`(默认256KB,配置字段白名单)限制,本次不改旧策略。
|
||||||
|
- 超限内容尾部追加 `...[HB_XDR_TRUNCATED original_utf8_bytes=原字节数]`,标记包含在2048字节内,
|
||||||
|
不切断中文或emoji。不会修改规则输入或InfluxDB原文限制;大日志通道不应用2KB限制,仍沿用旧策略。
|
||||||
|
- 旧的 `dm.normal.large-field-max-utf8-bytes` 和 `dm.non-normal.large-field-max-utf8-bytes` 继续生效;
|
||||||
|
同时命中时最终遵循较小上限。此配置不修改历史记录,已截掉的字段内容不能靠回滚JAR恢复。
|
||||||
|
|
||||||
|
替换JAR并重启即可,无需数据库或Kafka脚本。若容器已有 `SYSLOG_RAW_LISTENER_FIELD_MAX_UTF8_BYTES`,
|
||||||
|
环境变量优先于JAR内默认值。启动日志 `SYSLOG_BUILD_READY` 的 `rawFieldMaxUtf8Bytes=2048` 可确认实际值。
|
||||||
|
|
||||||
## 回滚
|
## 回滚
|
||||||
|
|
||||||
回滚旧JAR会恢复主消费同步入口统计、事实计数Outbox、同步审计及分钟桶保护,新独立listener随进程退出停止。
|
回滚旧JAR会恢复主消费同步入口统计、事实计数Outbox、同步审计及分钟桶保护,新独立listener随进程退出停止。
|
||||||
|
|||||||
Reference in New Issue
Block a user