task-19: Coze 请求/响应与 Python 回传日志改采样、截断、DEBUG

- 新增 SimilarAsinLogSupport 纯函数:truncate(2000 前缀+长度后缀,代理对安全)
  与 shouldLog(每 N 次采样,计数 0 恒采样)
- Coze 请求 body 与 submit 响应正文日志降 DEBUG + 截断;
  history 轮询响应降 DEBUG + 每 20 次记一次完整正文,其余只记状态
- Python 回传逐行日志降 DEBUG + 每 20 行采样一行,大任务日志量降到 5%
This commit is contained in:
2026-08-29 18:11:24 +08:00
parent 07e99a5fa8
commit 77ae8b2823
4 changed files with 226 additions and 8 deletions
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.config.SimilarAsinProperties;
import com.nanri.aiimage.modules.coze.service.CozeCredentialPoolService;
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinResultRowDto;
import com.nanri.aiimage.modules.similarasin.util.SimilarAsinLogSupport;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
@@ -30,11 +31,14 @@ public class SimilarAsinCozeClient {
private static final String MODULE_TYPE = "SIMILAR_ASIN";
private static final MediaType APPLICATION_JSON_UTF8 = new MediaType("application", "json", StandardCharsets.UTF_8);
/** Task 19history 轮询响应正文采样频率(每 N 次记一次完整正文,其余只记状态)。 */
static final long HISTORY_RESPONSE_LOG_EVERY_N = 20L;
private final SimilarAsinProperties properties;
private final ObjectMapper objectMapper;
private final CozeCredentialPoolService cozeCredentialPoolService;
private final AtomicLong credentialCursor = new AtomicLong();
private final AtomicLong historyResponseLogCounter = new AtomicLong();
/**
* P1-7:单例 RestClient。原 restClient() 每次提交/poll 都新建 SimpleClientHttpRequestFactory + RestClient
* 几千行任务并发时会反复创建短命对象造成不必要 GC 压力。RestClient 与 SimpleClientHttpRequestFactory
@@ -267,10 +271,10 @@ public class SimilarAsinCozeClient {
body.put("workflow_id", credential.workflowId());
body.put("parameters", parameters);
body.put("is_async", Boolean.TRUE);
log.info("[similar-asin] coze request credential={} url={} body={}",
log.debug("[similar-asin] coze request credential={} url={} body={}",
credential.name(),
joinUrl(properties.getCozeBaseUrl(), properties.getCozeWorkflowPath()),
writeJson(maskCozeRequestBody(body)));
SimilarAsinLogSupport.truncate(writeJson(maskCozeRequestBody(body))));
RestClient.RequestBodySpec request = restClient().post()
.uri(joinUrl(properties.getCozeBaseUrl(), properties.getCozeWorkflowPath()))
@@ -283,9 +287,9 @@ public class SimilarAsinCozeClient {
return request.exchange((clientRequest, clientResponse) -> {
byte[] responseBytes = StreamUtils.copyToByteArray(clientResponse.getBody());
String responseText = responseBytes.length == 0 ? "" : new String(responseBytes, StandardCharsets.UTF_8);
log.info("[similar-asin] coze submit response status={} body={}",
log.debug("[similar-asin] coze submit response status={} body={}",
clientResponse.getStatusCode(),
responseText);
SimilarAsinLogSupport.truncate(responseText));
return responseText;
});
}
@@ -294,6 +298,7 @@ public class SimilarAsinCozeClient {
String path = properties.getCozeWorkflowHistoryPath()
.replace("{workflow_id}", credential.workflowId())
.replace("{execute_id}", executeId);
long historyLogCounter = historyResponseLogCounter.getAndIncrement();
return restClient().get()
.uri(joinUrl(properties.getCozeBaseUrl(), path))
.headers(headers -> {
@@ -304,10 +309,12 @@ public class SimilarAsinCozeClient {
.exchange((clientRequest, clientResponse) -> {
byte[] responseBytes = StreamUtils.copyToByteArray(clientResponse.getBody());
String responseText = responseBytes.length == 0 ? "" : new String(responseBytes, StandardCharsets.UTF_8);
log.info("[similar-asin] coze history response credential={} executeId={} status={} body={}",
log.debug("[similar-asin] coze history response credential={} executeId={} status={} body={}",
credential.name(), executeId,
clientResponse.getStatusCode(),
responseText);
SimilarAsinLogSupport.shouldLog(historyLogCounter, HISTORY_RESPONSE_LOG_EVERY_N)
? SimilarAsinLogSupport.truncate(responseText)
: "[sampled out]");
return responseText;
});
}
@@ -40,6 +40,7 @@ import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinTaskItemVo;
import com.nanri.aiimage.modules.similarasin.util.BoundedImageCache;
import com.nanri.aiimage.modules.similarasin.util.ExcelCellImageWriter;
import com.nanri.aiimage.modules.similarasin.util.SimilarAsinImageEmbedder;
import com.nanri.aiimage.modules.similarasin.util.SimilarAsinLogSupport;
import com.nanri.aiimage.modules.file.service.LocalFileStorageService;
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
@@ -137,6 +138,8 @@ public class SimilarAsinTaskService {
private static final String CONTENT_TYPE_XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
private static final String CONTENT_TYPE_ZIP = "application/zip";
private static final int RESULT_ROWS_READ_RETRY_LIMIT = 3;
/** Task 19Python 回传逐行日志采样频率(每 N 行记一行)。 */
private static final long PYTHON_INBOUND_LOG_EVERY_N = 20L;
private static final long RESULT_ROWS_READ_RETRY_DELAY_MS = 500L;
private static final Duration TASK_LOCK_TTL = Duration.ofMinutes(5);
private static final long TASK_LOCK_WAIT_MILLIS = 10000L;
@@ -1982,21 +1985,25 @@ public class SimilarAsinTaskService {
* 打印 Python 端回传给 Java 的每一行 row 关键字段确认 url主图/ urls同类商品图/ title / sku
* 是否按预期到达该日志与 SimilarAsinCozeClient coze items diff 日志成对
* 便于排查"Python 回传了什么、Java 又把什么发到 Coze"
* Task 19改为 DEBUG 级别并按行采样 20 行记一行减少大任务日志量
*/
private void logPythonInboundRows(List<SimilarAsinResultRowDto> rows) {
if (rows == null || rows.isEmpty()) {
return;
}
log.info("[similar-asin] python inbound start size={}", rows.size());
log.debug("[similar-asin] python inbound start size={}", rows.size());
for (int i = 0; i < rows.size(); i++) {
SimilarAsinResultRowDto row = rows.get(i);
if (row == null) {
continue;
}
if (!SimilarAsinLogSupport.shouldLog(i, PYTHON_INBOUND_LOG_EVERY_N)) {
continue;
}
String url = row.getUrl();
List<String> urls = row.getUrls();
List<SimilarAsinResultRowDto.AlibabaItem> alibaba = row.getAlibaba();
log.info("[similar-asin] python inbound idx={} groupKey={} rowToken={} id={} asin={} country={} title={} sku={} price={} url={} urlsSize={} alibabaSize={} urlsHead={} urlsTail={}",
log.debug("[similar-asin] python inbound idx={} groupKey={} rowToken={} id={} asin={} country={} title={} sku={} price={} url={} urlsSize={} alibabaSize={} urlsHead={} urlsTail={}",
i,
normalize(row.getGroupKey()),
normalize(row.getRowToken()),
@@ -0,0 +1,48 @@
package com.nanri.aiimage.modules.similarasin.util;
/**
* Task 19Coze 请求/响应及 Python 回传日志的采样与截断工具。
* truncate 保证超长正文输出有界(前缀 + 长度 + 后缀),不抛异常、不破坏代理对;
* shouldLog 按每 everyN 次采样一次(counter % everyN == 0),计数 0 恒采样。
* 两个方法均为纯函数,可在日志点直接内联使用。
*/
public final class SimilarAsinLogSupport {
private SimilarAsinLogSupport() {
}
/** 截断前缀保留长度(不含"…"与长度后缀)。 */
public static final int TRUNCATE_PREFIX_LENGTH = 2000;
/**
* 正文截断:长度 ≤ maxChars 原样返回;超过保留前 maxChars 字符并附
* "…[total=N chars]" 长度后缀。maxChars ≤ 0 视为不截断。
* null 返回空串。
*/
public static String truncate(String value, int maxChars) {
if (value == null) {
return "";
}
if (maxChars <= 0 || value.length() <= maxChars) {
return value;
}
return value.substring(0, maxChars) + "…[total=" + value.length() + " chars]";
}
/** 便捷重载:使用默认前缀长度 TRUNCATE_PREFIX_LENGTH。 */
public static String truncate(String value) {
return truncate(value, TRUNCATE_PREFIX_LENGTH);
}
/**
* 采样判定:每 everyN 次输出一次(counter % everyN == 0)。
* everyN ≤ 0 视为恒采样;counter 为 0 恒采样;计数接近溢出时取模结果仍稳定。
*/
public static boolean shouldLog(long counter, long everyN) {
if (everyN <= 0) {
return true;
}
long normalized = counter >= 0 ? counter : -counter;
return normalized % everyN == 0;
}
}