task-178: 日志字段规范落地(StructuredLog 规范字段集+缺失占位+值消毒,字段名冻结)+ 8 条测试

- 规范字段 traceId/taskId/moduleType/stage/submissionId/chunkIndex/chunkTotal/jobId/result/errorType,顺序与 spec11 §2 一致
- format 只输出规范字段、缺失用 "-" 占位、换行/控制字符消毒、超长截断,防日志注入与字段漂移
- 源码锚点断言 Worker(task-file-job)/心跳(task-heartbeat) 代表性日志已带 taskId/moduleType 核心字段;不删现有日志
This commit is contained in:
2026-09-04 23:37:50 +08:00
parent 05da03c5ba
commit 620ee15a48
2 changed files with 173 additions and 0 deletions
@@ -0,0 +1,72 @@
package com.nanri.aiimage.config;
import java.util.List;
import java.util.Map;
/**
* 业务日志结构化字段规范(task-178,spec 11 §2)。
*
* 规范字段集与缺失占位:业务日志统一携带 traceId/taskId/moduleType/stage/submissionId/
* chunkIndex/chunkTotal/jobId/result/errorType;某条日志缺失某字段用 {@link #MISSING} 占位,
* 保证字段名稳定、可检索。值写入前消毒(去除换行/控制字符、超长截断),防止日志注入与字段错乱。
*
* 不删任何现有日志;本类作为字段名与渲染的单一来源,供后续可观测性埋点(180+)统一使用。
*/
public final class StructuredLog {
/** 规范字段名(顺序即输出顺序,与 spec 11 §2 一致)。 */
public static final List<String> FIELD_NAMES = List.of(
"traceId", "taskId", "moduleType", "stage",
"submissionId", "chunkIndex", "chunkTotal",
"jobId", "result", "errorType");
/** 缺失字段占位符。 */
public static final String MISSING = "-";
/** 单值最大展示长度(字节),避免超长字段撑爆日志。 */
public static final int MAX_VALUE_LENGTH = 256;
private StructuredLog() {
}
/** 渲染单个字段值:null/空 → 占位;消毒换行与控制字符;截断到上限。 */
public static String field(Object value) {
if (value == null) {
return MISSING;
}
String text = String.valueOf(value);
if (text.isEmpty()) {
return MISSING;
}
text = text.replace('\r', ' ').replace('\n', ' ').replace('\t', ' ');
StringBuilder cleaned = new StringBuilder(text.length());
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c < 0x20 && c != ' ') {
continue; // 丢弃其余控制字符
}
cleaned.append(c);
}
String result = cleaned.toString().trim();
if (result.length() > MAX_VALUE_LENGTH) {
result = result.substring(0, MAX_VALUE_LENGTH);
}
return result.isEmpty() ? MISSING : result;
}
/**
* 按规范字段集渲染 key=value 串:仅输出规范字段、顺序固定,缺失字段用占位符,
* 避免不同日志字段漂移。
*/
public static String format(Map<String, ?> fields) {
StringBuilder sb = new StringBuilder();
for (String name : FIELD_NAMES) {
if (sb.length() > 0) {
sb.append(' ');
}
Object value = fields == null ? null : fields.get(name);
sb.append(name).append('=').append(field(value));
}
return sb.toString();
}
}