| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179 |
- 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();
- }
- }
- }
|