package com.agent.management.config; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.client.ClientHttpRequestFactories; import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpRequest; import org.springframework.http.client.BufferingClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.web.client.RestClient; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.stream.Collectors; /** * Spring AI HTTP 请求/响应日志配置 * 通过自定义 RestClient.Builder 拦截 Spring AI 发出的所有 HTTP 请求 */ @Slf4j @Configuration public class AiRequestLoggingConfig { @Value("${spring.ai.openai.base-url:}") private String baseUrl; @Value("${spring.ai.openai.chat.completions-path:/v1/chat/completions}") private String completionsPath; @Value("${spring.ai.openai.chat.options.model:}") private String model; /** * 启动时打印 AI 连接配置 */ @PostConstruct public void logAiConfig() { log.info("[AI-CONFIG] ========== 大模型连接配置 =========="); log.info("[AI-CONFIG] base-url : {}", baseUrl); log.info("[AI-CONFIG] completions-path: {}", completionsPath); log.info("[AI-CONFIG] model : {}", model); log.info("[AI-CONFIG] 完整请求地址 : {}{}", baseUrl, completionsPath); log.info("[AI-CONFIG] ========================================="); } /** * 自定义 RestClient.Builder,注入日志拦截器 * Spring AI 1.0 的 OpenAiApi 使用 RestClient 发请求,会使用此 Builder */ @Bean public RestClient.Builder restClientBuilder() { SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); requestFactory.setConnectTimeout(Duration.ofSeconds(10)); requestFactory.setReadTimeout(Duration.ofSeconds(120)); return RestClient.builder() .requestFactory(new BufferingClientHttpRequestFactory(requestFactory)) .requestInterceptor(new AiLoggingInterceptor()); } /** * HTTP 请求/响应日志拦截器 */ static class AiLoggingInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { String url = request.getURI().toString(); // === 打印请求信息 === log.info("[AI-HTTP] ========== 请求开始 =========="); log.info("[AI-HTTP] 请求方法: {}", request.getMethod()); log.info("[AI-HTTP] 请求 URL: {}", request.getURI()); log.info("[AI-HTTP] 请求 Headers:"); request.getHeaders().forEach((name, values) -> { // 脱敏处理 Authorization / api-key if (name.toLowerCase().contains("authorization") || name.toLowerCase().contains("api-key")) { String val = values.iterator().next(); String masked = val.length() > 10 ? val.substring(0, 6) + "***" + val.substring(val.length() - 4) : "***"; log.info("[AI-HTTP] {}: {}", name, masked); } else { log.info("[AI-HTTP] {}: {}", name, values); } }); String requestBody = new String(body, StandardCharsets.UTF_8); log.info("[AI-HTTP] 请求 Body ({} bytes): {}", body.length, truncate(requestBody, 2000)); // === 执行请求 === long startTime = System.currentTimeMillis(); ClientHttpResponse response; try { response = execution.execute(request, body); } catch (IOException e) { long elapsed = System.currentTimeMillis() - startTime; log.error("[AI-HTTP] 请求失败, URL: {}, 耗时: {}ms, 异常: {}", url, elapsed, e.getMessage()); log.error("[AI-HTTP] 异常堆栈:", e); throw e; } long elapsed = System.currentTimeMillis() - startTime; // === 打印响应信息 === log.info("[AI-HTTP] 响应状态: {} {}, 耗时: {}ms, URL: {}", response.getStatusCode().value(), response.getStatusText(), elapsed, url); log.info("[AI-HTTP] 响应 Headers:"); response.getHeaders().forEach((name, values) -> log.info("[AI-HTTP] {}: {}", name, values)); // 读取响应 body String responseBody; try (BufferedReader reader = new BufferedReader( new InputStreamReader(response.getBody(), StandardCharsets.UTF_8))) { responseBody = reader.lines().collect(Collectors.joining("\n")); } log.info("[AI-HTTP] 响应 Body ({} chars): {}", responseBody.length(), truncate(responseBody, 3000)); log.info("[AI-HTTP] ========== 请求结束 =========="); // 返回可重复读取的响应 return new RepeatableClientHttpResponse(response, responseBody); } private String truncate(String text, int maxLen) { if (text == null) return "null"; if (text.length() <= maxLen) return text; return text.substring(0, maxLen) + "...(截断,共 " + text.length() + " 字符)"; } } /** * 可重复读取的 ClientHttpResponse 包装类 */ static class RepeatableClientHttpResponse implements ClientHttpResponse { private final ClientHttpResponse original; private final String body; RepeatableClientHttpResponse(ClientHttpResponse original, String body) { this.original = original; this.body = body; } @Override public org.springframework.http.HttpHeaders getHeaders() { return original.getHeaders(); } @Override public java.io.InputStream getBody() { return new java.io.ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)); } @Override public org.springframework.http.HttpStatusCode getStatusCode() throws IOException { return original.getStatusCode(); } @Override public String getStatusText() throws IOException { return original.getStatusText(); } @Override public void close() { original.close(); } } }