AiRequestLoggingConfig.java 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. package com.agent.management.config;
  2. import jakarta.annotation.PostConstruct;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.springframework.beans.factory.annotation.Value;
  5. import org.springframework.boot.web.client.ClientHttpRequestFactories;
  6. import org.springframework.boot.web.client.ClientHttpRequestFactorySettings;
  7. import org.springframework.context.annotation.Bean;
  8. import org.springframework.context.annotation.Configuration;
  9. import org.springframework.http.HttpRequest;
  10. import org.springframework.http.client.BufferingClientHttpRequestFactory;
  11. import org.springframework.http.client.ClientHttpRequestExecution;
  12. import org.springframework.http.client.ClientHttpRequestInterceptor;
  13. import org.springframework.http.client.ClientHttpResponse;
  14. import org.springframework.http.client.SimpleClientHttpRequestFactory;
  15. import org.springframework.web.client.RestClient;
  16. import java.io.BufferedReader;
  17. import java.io.IOException;
  18. import java.io.InputStreamReader;
  19. import java.nio.charset.StandardCharsets;
  20. import java.time.Duration;
  21. import java.util.stream.Collectors;
  22. /**
  23. * Spring AI HTTP 请求/响应日志配置
  24. * 通过自定义 RestClient.Builder 拦截 Spring AI 发出的所有 HTTP 请求
  25. */
  26. @Slf4j
  27. @Configuration
  28. public class AiRequestLoggingConfig {
  29. @Value("${spring.ai.openai.base-url:}")
  30. private String baseUrl;
  31. @Value("${spring.ai.openai.chat.completions-path:/v1/chat/completions}")
  32. private String completionsPath;
  33. @Value("${spring.ai.openai.chat.options.model:}")
  34. private String model;
  35. /**
  36. * 启动时打印 AI 连接配置
  37. */
  38. @PostConstruct
  39. public void logAiConfig() {
  40. log.info("[AI-CONFIG] ========== 大模型连接配置 ==========");
  41. log.info("[AI-CONFIG] base-url : {}", baseUrl);
  42. log.info("[AI-CONFIG] completions-path: {}", completionsPath);
  43. log.info("[AI-CONFIG] model : {}", model);
  44. log.info("[AI-CONFIG] 完整请求地址 : {}{}", baseUrl, completionsPath);
  45. log.info("[AI-CONFIG] =========================================");
  46. }
  47. /**
  48. * 自定义 RestClient.Builder,注入日志拦截器
  49. * Spring AI 1.0 的 OpenAiApi 使用 RestClient 发请求,会使用此 Builder
  50. */
  51. @Bean
  52. public RestClient.Builder restClientBuilder() {
  53. SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
  54. requestFactory.setConnectTimeout(Duration.ofSeconds(10));
  55. requestFactory.setReadTimeout(Duration.ofSeconds(120));
  56. return RestClient.builder()
  57. .requestFactory(new BufferingClientHttpRequestFactory(requestFactory))
  58. .requestInterceptor(new AiLoggingInterceptor());
  59. }
  60. /**
  61. * HTTP 请求/响应日志拦截器
  62. */
  63. static class AiLoggingInterceptor implements ClientHttpRequestInterceptor {
  64. @Override
  65. public ClientHttpResponse intercept(HttpRequest request, byte[] body,
  66. ClientHttpRequestExecution execution) throws IOException {
  67. String url = request.getURI().toString();
  68. // === 打印请求信息 ===
  69. log.info("[AI-HTTP] ========== 请求开始 ==========");
  70. log.info("[AI-HTTP] 请求方法: {}", request.getMethod());
  71. log.info("[AI-HTTP] 请求 URL: {}", request.getURI());
  72. log.info("[AI-HTTP] 请求 Headers:");
  73. request.getHeaders().forEach((name, values) -> {
  74. // 脱敏处理 Authorization / api-key
  75. if (name.toLowerCase().contains("authorization") || name.toLowerCase().contains("api-key")) {
  76. String val = values.iterator().next();
  77. String masked = val.length() > 10
  78. ? val.substring(0, 6) + "***" + val.substring(val.length() - 4)
  79. : "***";
  80. log.info("[AI-HTTP] {}: {}", name, masked);
  81. } else {
  82. log.info("[AI-HTTP] {}: {}", name, values);
  83. }
  84. });
  85. String requestBody = new String(body, StandardCharsets.UTF_8);
  86. log.info("[AI-HTTP] 请求 Body ({} bytes): {}", body.length, truncate(requestBody, 2000));
  87. // === 执行请求 ===
  88. long startTime = System.currentTimeMillis();
  89. ClientHttpResponse response;
  90. try {
  91. response = execution.execute(request, body);
  92. } catch (IOException e) {
  93. long elapsed = System.currentTimeMillis() - startTime;
  94. log.error("[AI-HTTP] 请求失败, URL: {}, 耗时: {}ms, 异常: {}", url, elapsed, e.getMessage());
  95. log.error("[AI-HTTP] 异常堆栈:", e);
  96. throw e;
  97. }
  98. long elapsed = System.currentTimeMillis() - startTime;
  99. // === 打印响应信息 ===
  100. log.info("[AI-HTTP] 响应状态: {} {}, 耗时: {}ms, URL: {}",
  101. response.getStatusCode().value(), response.getStatusText(), elapsed, url);
  102. log.info("[AI-HTTP] 响应 Headers:");
  103. response.getHeaders().forEach((name, values) ->
  104. log.info("[AI-HTTP] {}: {}", name, values));
  105. // 读取响应 body
  106. String responseBody;
  107. try (BufferedReader reader = new BufferedReader(
  108. new InputStreamReader(response.getBody(), StandardCharsets.UTF_8))) {
  109. responseBody = reader.lines().collect(Collectors.joining("\n"));
  110. }
  111. log.info("[AI-HTTP] 响应 Body ({} chars): {}", responseBody.length(), truncate(responseBody, 3000));
  112. log.info("[AI-HTTP] ========== 请求结束 ==========");
  113. // 返回可重复读取的响应
  114. return new RepeatableClientHttpResponse(response, responseBody);
  115. }
  116. private String truncate(String text, int maxLen) {
  117. if (text == null) return "null";
  118. if (text.length() <= maxLen) return text;
  119. return text.substring(0, maxLen) + "...(截断,共 " + text.length() + " 字符)";
  120. }
  121. }
  122. /**
  123. * 可重复读取的 ClientHttpResponse 包装类
  124. */
  125. static class RepeatableClientHttpResponse implements ClientHttpResponse {
  126. private final ClientHttpResponse original;
  127. private final String body;
  128. RepeatableClientHttpResponse(ClientHttpResponse original, String body) {
  129. this.original = original;
  130. this.body = body;
  131. }
  132. @Override
  133. public org.springframework.http.HttpHeaders getHeaders() {
  134. return original.getHeaders();
  135. }
  136. @Override
  137. public java.io.InputStream getBody() {
  138. return new java.io.ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
  139. }
  140. @Override
  141. public org.springframework.http.HttpStatusCode getStatusCode() throws IOException {
  142. return original.getStatusCode();
  143. }
  144. @Override
  145. public String getStatusText() throws IOException {
  146. return original.getStatusText();
  147. }
  148. @Override
  149. public void close() {
  150. original.close();
  151. }
  152. }
  153. }