스프링 부트 - 예외를 포함한 모든 요청 및 응답을 한 곳에 기록하는 방법
나는 봄 부츠로 레스트 api 작업을 하고 있어.모든 요청을 입력 매개 변수(예: GET, POST 등), 요청 경로, 쿼리 문자열, 이 요청의 해당 클래스 방법, 또한 이 작업의 응답, 성공 및 오류 모두로 기록할 필요가 있다.예를 들면 다음과 같다.
성공적인 요청:
http://example.com/api/users/1
로그는 다음과 같이 표시되어야 한다.
{
HttpStatus: 200,
path: "api/users/1",
method: "GET",
clientIp: "0.0.0.0",
accessToken: "XHGu6as5dajshdgau6i6asdjhgjhg",
method: "UsersController.getUser",
arguments: {
id: 1
},
response: {
user: {
id: 1,
username: "user123",
email: "user123@example.com"
}
},
exceptions: []
}
또는 오류가 있는 요청:
http://example.com/api/users/9999
로그는 다음과 같아야 한다.
{
HttpStatus: 404,
errorCode: 101,
path: "api/users/9999",
method: "GET",
clientIp: "0.0.0.0",
accessToken: "XHGu6as5dajshdgau6i6asdjhgjhg",
method: "UsersController.getUser",
arguments: {
id: 9999
},
returns: {
},
exceptions: [
{
exception: "UserNotFoundException",
message: "User with id 9999 not found",
exceptionId: "adhaskldjaso98d7324kjh989",
stacktrace: ...................
]
}
요청/응답은 성공 사례와 오류 사례 모두에서 이 엔티티와 관련된 사용자 정의 정보를 포함하는 단일 엔티티가 되기를 원한다.
이를 달성하기 위한 봄에 가장 좋은 방법은 무엇인가? 필터가 있을 수 있다.만약 그렇다면, 너는 구체적인 예를 들어줄 수 있니?
같이 놀아본 적이 있다.@ControllerAdvice
그리고@ExceptionHandler
, 그러나 내가 말했듯이, 나는 모든 성공과 오류 요청을 한 장소에서 처리해야 한다.
인터셉터, 필터, 구성 요소, 측면 등을 작성하지 마십시오. 이것은 매우 일반적인 문제로서 여러 번 해결되었다.
스프링 부트는 액츄에이터라는 모듈을 가지고 있는데, 이 모듈은 박스에서 HTTP 요청 로그아웃을 제공한다.에 매핑된 엔드포인트가/trace
SB1 (SB1.x)는/actuator/httptrace
SB2SB2.0+) 마지막 100개의 HTTP 요청을 보여 줄 것이다.각 요청을 기록하거나 DB에 쓰도록 사용자 정의할 수 있다.
원하는 엔드포인트를 얻으려면 스프링-부팅-스타터-액튜레이터 종속성이 필요하며, 또한 찾고 있는 엔드포인트를 "화이트리스트"해야 하며, 이에 대한 보안을 설정하거나 해제해야 할 수도 있다.
또한, 이 애플리케이션은 어디에서 실행될 것인가?PaaS를 사용하시겠습니까?예를 들어 Heroku와 같은 호스팅 제공자들은 그들의 서비스의 일부로 요청 로깅을 제공하므로 당신은 어떠한 코딩도 할 필요가 없다.
스프링은 이미 이 작업을 수행하는 필터를 제공하고 있다.구성에 다음 빈 추가
@Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
loggingFilter.setIncludeClientInfo(true);
loggingFilter.setIncludeQueryString(true);
loggingFilter.setIncludePayload(true);
loggingFilter.setMaxPayloadLength(64000);
return loggingFilter;
}
로그 수준 변경 잊지 마십시오.org.springframework.web.filter.CommonsRequestLoggingFilter
로DEBUG
.
당신은 사용할 수 있다.javax.servlet.Filter
실행된 Java 메서드를 기록할 필요가 없는 경우.
하지만 이 요구 사항으로 당신은 에 저장된 정보에 액세스해야 한다.handlerMapping
의DispatcherServlet
그렇긴 하지만, 당신은 무효로 할 수 있다.DispatcherServlet
요청/응답 쌍의 로깅을 완료한다.
아래는 당신의 필요에 따라 더욱 강화되고 채택될 수 있는 아이디어의 예다.
public class LoggableDispatcherServlet extends DispatcherServlet {
private final Log logger = LogFactory.getLog(getClass());
@Override
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (!(request instanceof ContentCachingRequestWrapper)) {
request = new ContentCachingRequestWrapper(request);
}
if (!(response instanceof ContentCachingResponseWrapper)) {
response = new ContentCachingResponseWrapper(response);
}
HandlerExecutionChain handler = getHandler(request);
try {
super.doDispatch(request, response);
} finally {
log(request, response, handler);
updateResponse(response);
}
}
private void log(HttpServletRequest requestToCache, HttpServletResponse responseToCache, HandlerExecutionChain handler) {
LogMessage log = new LogMessage();
log.setHttpStatus(responseToCache.getStatus());
log.setHttpMethod(requestToCache.getMethod());
log.setPath(requestToCache.getRequestURI());
log.setClientIp(requestToCache.getRemoteAddr());
log.setJavaMethod(handler.toString());
log.setResponse(getResponsePayload(responseToCache));
logger.info(log);
}
private String getResponsePayload(HttpServletResponse response) {
ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
if (wrapper != null) {
byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
int length = Math.min(buf.length, 5120);
try {
return new String(buf, 0, length, wrapper.getCharacterEncoding());
}
catch (UnsupportedEncodingException ex) {
// NOOP
}
}
}
return "[unknown]";
}
private void updateResponse(HttpServletResponse response) throws IOException {
ContentCachingResponseWrapper responseWrapper =
WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
responseWrapper.copyBodyToResponse();
}
}
HandlerExecutionChain
- 요청 처리기에 대한 정보를 포함한다.
그런 다음 이 발송인을 다음과 같이 등록할 수 있다.
@Bean
public ServletRegistrationBean dispatcherRegistration() {
return new ServletRegistrationBean(dispatcherServlet());
}
@Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet() {
return new LoggableDispatcherServlet();
}
여기 로그의 샘플이 있다.
http http://localhost:8090/settings/test
i.g.m.s.s.LoggableDispatcherServlet : LogMessage{httpStatus=500, path='/error', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)] and 3 interceptors', arguments=null, response='{"timestamp":1472475814077,"status":500,"error":"Internal Server Error","exception":"java.lang.RuntimeException","message":"org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.RuntimeException","path":"/settings/test"}'}
http http://localhost:8090/settings/params
i.g.m.s.s.LoggableDispatcherServlet : LogMessage{httpStatus=200, path='/settings/httpParams', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [public x.y.z.DTO x.y.z.Controller.params()] and 3 interceptors', arguments=null, response='{}'}
http http://localhost:8090/123
i.g.m.s.s.LoggableDispatcherServlet : LogMessage{httpStatus=404, path='/error', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)] and 3 interceptors', arguments=null, response='{"timestamp":1472475840592,"status":404,"error":"Not Found","message":"Not Found","path":"/123"}'}
갱신하다
오류가 발생할 경우 Spring은 자동 오류 처리를 한다.그러므로BasicErrorController#error
요청 처리기로 표시된다.원본 요청 처리기를 보존하려면 다음 위치에서 이 동작을 재정의하십시오.spring-webmvc-4.2.5.RELEASE-sources.jar!/org/springframework/web/servlet/DispatcherServlet.java:971
전에#processDispatchResult
원래 핸들러를 캐시하기 위해 호출된다.
로그북 라이브러리는 HTTP 요청 및 응답을 기록하기 위해 특별히 만들어졌다.특수 스타터 라이브러리를 사용하여 스프링 부트를 지원한다.
스프링 부트에서 로깅을 사용하려면 프로젝트의 종속성에 라이브러리를 추가하기만 하면 된다.예를 들어, Maven을 사용한다고 가정할 경우:
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-spring-boot-starter</artifactId>
<version>1.5.0</version>
</dependency>
기본적으로 로깅 출력은 다음과 같다.
{
"origin" : "local",
"correlation" : "52e19498-890c-4f75-a06c-06ddcf20836e",
"status" : 200,
"headers" : {
"X-Application-Context" : [
"application:8088"
],
"Content-Type" : [
"application/json;charset=UTF-8"
],
"Transfer-Encoding" : [
"chunked"
],
"Date" : [
"Sun, 24 Dec 2017 13:10:45 GMT"
]
},
"body" : {
"thekey" : "some_example"
},
"duration" : 105,
"protocol" : "HTTP/1.1",
"type" : "response"
}
그러나 요청을 처리하는 클래스 이름은 출력하지 않는다.도서관은 사용자 정의 로거를 쓰기 위한 몇몇 인터페이스를 가지고 있다.
메모들
도서관이 크게 발전한 동안 현재 버전은 2.4.1이다. https://github.com/zalando/logbook/releases을 참조하십시오.예: 기본 ouput 형식이 변경되었으며, 구성, 필터링 등이 가능하다.
로그 수준을 다음으로 설정하는 것을 잊지 마십시오.TRACE
, 그렇지 않으면 아무것도 볼 수 없을 것이다.
logging:
level:
org.zalando.logbook: TRACE
로그 수준을 정의한 경우application.properties
로그 파일에 요청/해제, 메서드 URL을 인쇄하려면
logging.level.org.springframework.web=DEBUG
logging.level.org.hibernate.SQL=INFO
logging.file=D:/log/myapp.log
나는 스프링 부트를 사용했었다.
봄철 데이터 쉼터에서 사용하는 방법은 다음과 같다.org.springframework.web.util.ContentCachingRequestWrapper
그리고org.springframework.web.util.ContentCachingResponseWrapper
/**
* Doogies very cool HTTP request logging
*
* There is also {@link org.springframework.web.filter.CommonsRequestLoggingFilter} but it cannot log request method
* And it cannot easily be extended.
*
* https://mdeinum.wordpress.com/2015/07/01/spring-framework-hidden-gems/
* http://stackoverflow.com/questions/8933054/how-to-read-and-copy-the-http-servlet-response-output-stream-content-for-logging
*/
public class DoogiesRequestLogger extends OncePerRequestFilter {
private boolean includeResponsePayload = true;
private int maxPayloadLength = 1000;
private String getContentAsString(byte[] buf, int maxLength, String charsetName) {
if (buf == null || buf.length == 0) return "";
int length = Math.min(buf.length, this.maxPayloadLength);
try {
return new String(buf, 0, length, charsetName);
} catch (UnsupportedEncodingException ex) {
return "Unsupported Encoding";
}
}
/**
* Log each request and respponse with full Request URI, content payload and duration of the request in ms.
* @param request the request
* @param response the response
* @param filterChain chain of filters
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
StringBuffer reqInfo = new StringBuffer()
.append("[")
.append(startTime % 10000) // request ID
.append("] ")
.append(request.getMethod())
.append(" ")
.append(request.getRequestURL());
String queryString = request.getQueryString();
if (queryString != null) {
reqInfo.append("?").append(queryString);
}
if (request.getAuthType() != null) {
reqInfo.append(", authType=")
.append(request.getAuthType());
}
if (request.getUserPrincipal() != null) {
reqInfo.append(", principalName=")
.append(request.getUserPrincipal().getName());
}
this.logger.debug("=> " + reqInfo);
// ========= Log request and response payload ("body") ========
// We CANNOT simply read the request payload here, because then the InputStream would be consumed and cannot be read again by the actual processing/server.
// String reqBody = DoogiesUtil._stream2String(request.getInputStream()); // THIS WOULD NOT WORK!
// So we need to apply some stronger magic here :-)
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
filterChain.doFilter(wrappedRequest, wrappedResponse); // ======== This performs the actual request!
long duration = System.currentTimeMillis() - startTime;
// I can only log the request's body AFTER the request has been made and ContentCachingRequestWrapper did its work.
String requestBody = this.getContentAsString(wrappedRequest.getContentAsByteArray(), this.maxPayloadLength, request.getCharacterEncoding());
if (requestBody.length() > 0) {
this.logger.debug(" Request body:\n" +requestBody);
}
this.logger.debug("<= " + reqInfo + ": returned status=" + response.getStatus() + " in "+duration + "ms");
if (includeResponsePayload) {
byte[] buf = wrappedResponse.getContentAsByteArray();
this.logger.debug(" Response body:\n"+getContentAsString(buf, this.maxPayloadLength, response.getCharacterEncoding()));
}
wrappedResponse.copyBodyToResponse(); // IMPORTANT: copy content of response back into original response
}
}
이 코드는 Spring Boot 애플리케이션에서 내게 사용되므로 필터로 등록하십시오.
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.servlet.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.output.TeeOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class HttpLoggingFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(HttpLoggingFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
Map<String, String> requestMap = this
.getTypesafeRequestMap(httpServletRequest);
BufferedRequestWrapper bufferedRequest = new BufferedRequestWrapper(
httpServletRequest);
BufferedResponseWrapper bufferedResponse = new BufferedResponseWrapper(
httpServletResponse);
final StringBuilder logMessage = new StringBuilder(
"REST Request - ").append("[HTTP METHOD:")
.append(httpServletRequest.getMethod())
.append("] [PATH INFO:")
.append(httpServletRequest.getServletPath())
.append("] [REQUEST PARAMETERS:").append(requestMap)
.append("] [REQUEST BODY:")
.append(bufferedRequest.getRequestBody())
.append("] [REMOTE ADDRESS:")
.append(httpServletRequest.getRemoteAddr()).append("]");
chain.doFilter(bufferedRequest, bufferedResponse);
logMessage.append(" [RESPONSE:")
.append(bufferedResponse.getContent()).append("]");
log.debug(logMessage.toString());
} catch (Throwable a) {
log.error(a.getMessage());
}
}
private Map<String, String> getTypesafeRequestMap(HttpServletRequest request) {
Map<String, String> typesafeRequestMap = new HashMap<String, String>();
Enumeration<?> requestParamNames = request.getParameterNames();
while (requestParamNames.hasMoreElements()) {
String requestParamName = (String) requestParamNames.nextElement();
String requestParamValue;
if (requestParamName.equalsIgnoreCase("password")) {
requestParamValue = "********";
} else {
requestParamValue = request.getParameter(requestParamName);
}
typesafeRequestMap.put(requestParamName, requestParamValue);
}
return typesafeRequestMap;
}
@Override
public void destroy() {
}
private static final class BufferedRequestWrapper extends
HttpServletRequestWrapper {
private ByteArrayInputStream bais = null;
private ByteArrayOutputStream baos = null;
private BufferedServletInputStream bsis = null;
private byte[] buffer = null;
public BufferedRequestWrapper(HttpServletRequest req)
throws IOException {
super(req);
// Read InputStream and store its content in a buffer.
InputStream is = req.getInputStream();
this.baos = new ByteArrayOutputStream();
byte buf[] = new byte[1024];
int read;
while ((read = is.read(buf)) > 0) {
this.baos.write(buf, 0, read);
}
this.buffer = this.baos.toByteArray();
}
@Override
public ServletInputStream getInputStream() {
this.bais = new ByteArrayInputStream(this.buffer);
this.bsis = new BufferedServletInputStream(this.bais);
return this.bsis;
}
String getRequestBody() throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(
this.getInputStream()));
String line = null;
StringBuilder inputBuffer = new StringBuilder();
do {
line = reader.readLine();
if (null != line) {
inputBuffer.append(line.trim());
}
} while (line != null);
reader.close();
return inputBuffer.toString().trim();
}
}
private static final class BufferedServletInputStream extends
ServletInputStream {
private ByteArrayInputStream bais;
public BufferedServletInputStream(ByteArrayInputStream bais) {
this.bais = bais;
}
@Override
public int available() {
return this.bais.available();
}
@Override
public int read() {
return this.bais.read();
}
@Override
public int read(byte[] buf, int off, int len) {
return this.bais.read(buf, off, len);
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
}
public class TeeServletOutputStream extends ServletOutputStream {
private final TeeOutputStream targetStream;
public TeeServletOutputStream(OutputStream one, OutputStream two) {
targetStream = new TeeOutputStream(one, two);
}
@Override
public void write(int arg0) throws IOException {
this.targetStream.write(arg0);
}
public void flush() throws IOException {
super.flush();
this.targetStream.flush();
}
public void close() throws IOException {
super.close();
this.targetStream.close();
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
}
public class BufferedResponseWrapper implements HttpServletResponse {
HttpServletResponse original;
TeeServletOutputStream tee;
ByteArrayOutputStream bos;
public BufferedResponseWrapper(HttpServletResponse response) {
original = response;
}
public String getContent() {
return bos.toString();
}
public PrintWriter getWriter() throws IOException {
return original.getWriter();
}
public ServletOutputStream getOutputStream() throws IOException {
if (tee == null) {
bos = new ByteArrayOutputStream();
tee = new TeeServletOutputStream(original.getOutputStream(),
bos);
}
return tee;
}
@Override
public String getCharacterEncoding() {
return original.getCharacterEncoding();
}
@Override
public String getContentType() {
return original.getContentType();
}
@Override
public void setCharacterEncoding(String charset) {
original.setCharacterEncoding(charset);
}
@Override
public void setContentLength(int len) {
original.setContentLength(len);
}
@Override
public void setContentLengthLong(long l) {
original.setContentLengthLong(l);
}
@Override
public void setContentType(String type) {
original.setContentType(type);
}
@Override
public void setBufferSize(int size) {
original.setBufferSize(size);
}
@Override
public int getBufferSize() {
return original.getBufferSize();
}
@Override
public void flushBuffer() throws IOException {
tee.flush();
}
@Override
public void resetBuffer() {
original.resetBuffer();
}
@Override
public boolean isCommitted() {
return original.isCommitted();
}
@Override
public void reset() {
original.reset();
}
@Override
public void setLocale(Locale loc) {
original.setLocale(loc);
}
@Override
public Locale getLocale() {
return original.getLocale();
}
@Override
public void addCookie(Cookie cookie) {
original.addCookie(cookie);
}
@Override
public boolean containsHeader(String name) {
return original.containsHeader(name);
}
@Override
public String encodeURL(String url) {
return original.encodeURL(url);
}
@Override
public String encodeRedirectURL(String url) {
return original.encodeRedirectURL(url);
}
@SuppressWarnings("deprecation")
@Override
public String encodeUrl(String url) {
return original.encodeUrl(url);
}
@SuppressWarnings("deprecation")
@Override
public String encodeRedirectUrl(String url) {
return original.encodeRedirectUrl(url);
}
@Override
public void sendError(int sc, String msg) throws IOException {
original.sendError(sc, msg);
}
@Override
public void sendError(int sc) throws IOException {
original.sendError(sc);
}
@Override
public void sendRedirect(String location) throws IOException {
original.sendRedirect(location);
}
@Override
public void setDateHeader(String name, long date) {
original.setDateHeader(name, date);
}
@Override
public void addDateHeader(String name, long date) {
original.addDateHeader(name, date);
}
@Override
public void setHeader(String name, String value) {
original.setHeader(name, value);
}
@Override
public void addHeader(String name, String value) {
original.addHeader(name, value);
}
@Override
public void setIntHeader(String name, int value) {
original.setIntHeader(name, value);
}
@Override
public void addIntHeader(String name, int value) {
original.addIntHeader(name, value);
}
@Override
public void setStatus(int sc) {
original.setStatus(sc);
}
@SuppressWarnings("deprecation")
@Override
public void setStatus(int sc, String sm) {
original.setStatus(sc, sm);
}
@Override
public String getHeader(String arg0) {
return original.getHeader(arg0);
}
@Override
public Collection<String> getHeaderNames() {
return original.getHeaderNames();
}
@Override
public Collection<String> getHeaders(String arg0) {
return original.getHeaders(arg0);
}
@Override
public int getStatus() {
return original.getStatus();
}
}
}
만약 당신이 Spring AOP를 써보는 것을 꺼리지 않는다면, 이것은 내가 로깅을 위해 탐구해 온 것이고 그것은 나에게 꽤 효과가 있다.정의되지 않은 요청과 실패한 요청 시도는 기록하지 않는다.
다음 세 가지 종속성 추가
spring-aop, aspectjrt, aspectjweaver
을 xml에 하라.<aop:aspectj-autoproxy/>
점컷으로 사용할 수 있는 주석 작성
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface EnableLogging {
ActionType actionType();
}
이제 기록할 모든 나머지 API 메서드에 주석을 달으십시오.
@EnableLogging(actionType = ActionType.SOME_EMPLOYEE_ACTION)
@Override
public Response getEmployees(RequestDto req, final String param) {
...
}
이제 '양면' 구성 요소를 살펴보십시오. 이 클래스가 속한 패키지를 스캔하십시오.
@Aspect
@Component
public class Aspects {
@AfterReturning(pointcut = "execution(@co.xyz.aspect.EnableLogging * *(..)) && @annotation(enableLogging) && args(reqArg, reqArg1,..)", returning = "result")
public void auditInfo(JoinPoint joinPoint, Object result, EnableLogging enableLogging, Object reqArg, String reqArg1) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
if (result instanceof Response) {
Response responseObj = (Response) result;
String requestUrl = request.getScheme() + "://" + request.getServerName()
+ ":" + request.getServerPort() + request.getContextPath() + request.getRequestURI()
+ "?" + request.getQueryString();
String clientIp = request.getRemoteAddr();
String clientRequest = reqArg.toString();
int httpResponseStatus = responseObj.getStatus();
responseObj.getEntity();
// Can log whatever stuff from here in a single spot.
}
@AfterThrowing(pointcut = "execution(@co.xyz.aspect.EnableLogging * *(..)) && @annotation(enableLogging) && args(reqArg, reqArg1,..)", throwing="exception")
public void auditExceptionInfo(JoinPoint joinPoint, Throwable exception, EnableLogging enableLogging, Object reqArg, String reqArg1) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
String requestUrl = request.getScheme() + "://" + request.getServerName()
+ ":" + request.getServerPort() + request.getContextPath() + request.getRequestURI()
+ "?" + request.getQueryString();
exception.getMessage();
exception.getCause();
exception.printStackTrace();
exception.getLocalizedMessage();
// Can log whatever exceptions, requests, etc from here in a single spot.
}
}
@AfterReturning 조언은 일치된 방법 실행이 정상적으로 반환될 때 실행된다.
@AfterDrowing 조언은 예외를 던져 일치하는 방법 실행이 종료될 때 실행된다.
자세히 읽고 싶으면 이것을 읽어라.http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html
현재 Spring Boot에는 요청 및 응답 로그를 얻을 수 있는 액추에이터 기능이 있다.
하지만 당신은 또한 AOP를 사용하여 로그를 얻을 수 있다.
측면은 다음과 같은 주석을 제공한다.@Before
@AfterReturning
@AfterThrowing
등
@Before
요청을 기록한다.@AfterReturning
및 응답 기보 및@AfterThrowing
일부 필터를 패키지에 적용할 수 있도록 모든 엔드포인트의 로그가 필요하지 않을 수 있음 오류 메시지를 기록하십시오.
여기 몇 가지 예가 있다.
요청 시:
@Before("within(your.package.where.endpoints.are..*)")
public void endpointBefore(JoinPoint p) {
if (log.isTraceEnabled()) {
log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " START");
Object[] signatureArgs = p.getArgs();
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
try {
if (signatureArgs[0] != null) {
log.trace("\nRequest object: \n" + mapper.writeValueAsString(signatureArgs[0]));
}
} catch (JsonProcessingException e) {
}
}
}
여기@Before("within(your.package.where.endpoints.are..*)")
패키지 경로를 가지고 있다.이 패키지의 모든 엔드포인트가 로그를 생성한다.
응답의 경우:
@AfterReturning(value = ("within(your.package.where.endpoints.are..*)"),
returning = "returnValue")
public void endpointAfterReturning(JoinPoint p, Object returnValue) {
if (log.isTraceEnabled()) {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
try {
log.trace("\nResponse object: \n" + mapper.writeValueAsString(returnValue));
} catch (JsonProcessingException e) {
System.out.println(e.getMessage());
}
log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " END");
}
}
여기@AfterReturning("within(your.package.where.endpoints.are..*)")
패키지 경로를 가지고 있다.이 패키지의 모든 엔드포인트가 로그를 생성한다.또한Object returnValue
응답 내용을 포함한다.
예외의 경우:
@AfterThrowing(pointcut = ("within(your.package.where.endpoints.are..*)"), throwing = "e")
public void endpointAfterThrowing(JoinPoint p, Exception e) throws DmoneyException {
if (log.isTraceEnabled()) {
System.out.println(e.getMessage());
e.printStackTrace();
log.error(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " " + e.getMessage());
}
}
여기@AfterThrowing(pointcut = ("within(your.package.where.endpoints.are..*)"), throwing = "e")
패키지 경로를 가지고 있다.이 패키지의 모든 엔드포인트가 로그를 생성한다.또한Exception e
오류 응답을 포함한다.
다음은 전체 코드:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Order(1)
@Component
@ConditionalOnExpression("${endpoint.aspect.enabled:true}")
public class EndpointAspect {
static Logger log = Logger.getLogger(EndpointAspect.class);
@Before("within(your.package.where.is.endpoint..*)")
public void endpointBefore(JoinPoint p) {
if (log.isTraceEnabled()) {
log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " START");
Object[] signatureArgs = p.getArgs();
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
try {
if (signatureArgs[0] != null) {
log.trace("\nRequest object: \n" + mapper.writeValueAsString(signatureArgs[0]));
}
} catch (JsonProcessingException e) {
}
}
}
@AfterReturning(value = ("within(your.package.where.is.endpoint..*)"),
returning = "returnValue")
public void endpointAfterReturning(JoinPoint p, Object returnValue) {
if (log.isTraceEnabled()) {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
try {
log.trace("\nResponse object: \n" + mapper.writeValueAsString(returnValue));
} catch (JsonProcessingException e) {
System.out.println(e.getMessage());
}
log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " END");
}
}
@AfterThrowing(pointcut = ("within(your.package.where.is.endpoint..*)"), throwing = "e")
public void endpointAfterThrowing(JoinPoint p, Exception e) throws Exception {
if (log.isTraceEnabled()) {
System.out.println(e.getMessage());
e.printStackTrace();
log.error(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " " + e.getMessage());
}
}
}
여기서, 사용@ConditionalOnExpression("${endpoint.aspect.enabled:true}")
로그를 활성화/비활성화할 수 있다. 추가만 하면 된다.endpoint.aspect.enabled:true
의 안으로application.property
그리고 통나무를 제어한다.
AOP 방문에 대한 자세한 정보:
스프링 부트 베이스 애플리케이션에 액추에이터를 추가한 후/trace
최신 요청 정보가 있는 엔드포인트.이 엔드포인트는 TraceRepository를 기반으로 작동하며 기본 구현은 마지막 100개의 통화를 저장하는 InMemoryTraceRepository입니다.이 인터페이스를 직접 구현하여 봄콩으로 사용할 수 있게 함으로써 이것을 바꿀 수 있다.예를 들어, 로그에 대한 모든 요청을 기록(기본 구현을 여전히 정보 제공을 위한 기본 스토리지로 사용)/trace
엔드포인트) 다음과 같은 구현을 사용하고 있다.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.actuate.trace.InMemoryTraceRepository;
import org.springframework.boot.actuate.trace.Trace;
import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
@Component
public class LoggingTraceRepository implements TraceRepository {
private static final Logger LOG = LoggerFactory.getLogger(LoggingTraceRepository.class);
private final TraceRepository delegate = new InMemoryTraceRepository();
@Override
public List<Trace> findAll() {
return delegate.findAll();
}
@Override
public void add(Map<String, Object> traceInfo) {
LOG.info(traceInfo.toString());
this.delegate.add(traceInfo);
}
}
이것traceInfo
지도에는 다음과 같은 형식의 요청 및 응답에 대한 기본 정보가 포함되어 있다.{method=GET, path=/api/hello/John, headers={request={host=localhost:8080, user-agent=curl/7.51.0, accept=*/*}, response={X-Application-Context=application, Content-Type=text/plain;charset=UTF-8, Content-Length=10, Date=Wed, 29 Mar 2017 20:41:21 GMT, status=200}}}
에 응답 여기에 응답 내용이 없음.
편집! POST 데이터 로깅 중
WebRequestTraceFilter를 재정의하여 POST 데이터에 액세스할 수 있지만 좋은 아이디어라고 생각하지 마십시오(예: 업로드된 모든 파일 콘텐츠가 로그로 이동됨).다음은 샘플 코드지만 사용하지 마십시오.
package info.fingo.nuntius.acuate.trace;
import org.apache.commons.io.IOUtils;
import org.springframework.boot.actuate.trace.TraceProperties;
import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.boot.actuate.trace.WebRequestTraceFilter;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.LinkedHashMap;
import java.util.Map;
@Component
public class CustomWebTraceFilter extends WebRequestTraceFilter {
public CustomWebTraceFilter(TraceRepository repository, TraceProperties properties) {
super(repository, properties);
}
@Override
protected Map<String, Object> getTrace(HttpServletRequest request) {
Map<String, Object> trace = super.getTrace(request);
String multipartHeader = request.getHeader("content-type");
if (multipartHeader != null && multipartHeader.startsWith("multipart/form-data")) {
Map<String, Object> parts = new LinkedHashMap<>();
try {
request.getParts().forEach(
part -> {
try {
parts.put(part.getName(), IOUtils.toString(part.getInputStream(), Charset.forName("UTF-8")));
} catch (IOException e) {
e.printStackTrace();
}
}
);
} catch (IOException | ServletException e) {
e.printStackTrace();
}
if (!parts.isEmpty()) {
trace.put("multipart-content-map", parts);
}
}
return trace;
}
}
여기에서 해결 방법(2.0.x 스프링)
maven 종속성 추가:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
application.properties를 편집하고 다음 줄을 추가하십시오.
management.endpoints.web.exposure.include=*
스프링 부트 응용 프로그램이 시작되면 http://localhost:8070/actuator/htttrace URL로 전화를 걸어 최신 100개의 http 요청을 추적할 수 있음
실제 답변 https://gist.github.com/int128/e47217bebdb4c402b2ffa7cc199307ba은 아래 링크를 참조하십시오.
위에서 언급한 솔루션에서 몇 가지 변경사항을 적용하여 로거 레벨이 정보인 경우 요청 및 응답은 콘솔과 파일에도 로그인한다.우리는 콘솔이나 파일로 인쇄할 수 있다.
@Component
public class LoggingFilter extends OncePerRequestFilter {
private static final List<MediaType> VISIBLE_TYPES = Arrays.asList(
MediaType.valueOf("text/*"),
MediaType.APPLICATION_FORM_URLENCODED,
MediaType.APPLICATION_JSON,
MediaType.APPLICATION_XML,
MediaType.valueOf("application/*+json"),
MediaType.valueOf("application/*+xml"),
MediaType.MULTIPART_FORM_DATA
);
Logger log = LoggerFactory.getLogger(ReqAndResLoggingFilter.class);
private static final Path path = Paths.get("/home/ramesh/loggerReq.txt");
private static BufferedWriter writer = null;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
writer = Files.newBufferedWriter(path, Charset.forName("UTF-8"));
if (isAsyncDispatch(request)) {
filterChain.doFilter(request, response);
} else {
doFilterWrapped(wrapRequest(request), wrapResponse(response), filterChain);
}
}finally {
writer.close();
}
}
protected void doFilterWrapped(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response, FilterChain filterChain) throws ServletException, IOException {
try {
beforeRequest(request, response);
filterChain.doFilter(request, response);
}
finally {
afterRequest(request, response);
response.copyBodyToResponse();
}
}
protected void beforeRequest(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) throws IOException {
if (log.isInfoEnabled()) {
logRequestHeader(request, request.getRemoteAddr() + "|>");
}
}
protected void afterRequest(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) throws IOException {
if (log.isInfoEnabled()) {
logRequestBody(request, request.getRemoteAddr() + "|>");
logResponse(response, request.getRemoteAddr() + "|<");
}
}
private void logRequestHeader(ContentCachingRequestWrapper request, String prefix) throws IOException {
String queryString = request.getQueryString();
if (queryString == null) {
printLines(prefix,request.getMethod(),request.getRequestURI());
log.info("{} {} {}", prefix, request.getMethod(), request.getRequestURI());
} else {
printLines(prefix,request.getMethod(),request.getRequestURI(),queryString);
log.info("{} {} {}?{}", prefix, request.getMethod(), request.getRequestURI(), queryString);
}
Collections.list(request.getHeaderNames()).forEach(headerName ->
Collections.list(request.getHeaders(headerName)).forEach(headerValue ->
log.info("{} {}: {}", prefix, headerName, headerValue)));
printLines(prefix);
printLines(RequestContextHolder.currentRequestAttributes().getSessionId());
log.info("{}", prefix);
log.info(" Session ID: ", RequestContextHolder.currentRequestAttributes().getSessionId());
}
private void printLines(String ...args) throws IOException {
try {
for(String varArgs:args) {
writer.write(varArgs);
writer.newLine();
}
}catch(IOException ex){
ex.printStackTrace();
}
}
private void logRequestBody(ContentCachingRequestWrapper request, String prefix) {
byte[] content = request.getContentAsByteArray();
if (content.length > 0) {
logContent(content, request.getContentType(), request.getCharacterEncoding(), prefix);
}
}
private void logResponse(ContentCachingResponseWrapper response, String prefix) throws IOException {
int status = response.getStatus();
printLines(prefix, String.valueOf(status), HttpStatus.valueOf(status).getReasonPhrase());
log.info("{} {} {}", prefix, status, HttpStatus.valueOf(status).getReasonPhrase());
response.getHeaderNames().forEach(headerName ->
response.getHeaders(headerName).forEach(headerValue ->
log.info("{} {}: {}", prefix, headerName, headerValue)));
printLines(prefix);
log.info("{}", prefix);
byte[] content = response.getContentAsByteArray();
if (content.length > 0) {
logContent(content, response.getContentType(), response.getCharacterEncoding(), prefix);
}
}
private void logContent(byte[] content, String contentType, String contentEncoding, String prefix) {
MediaType mediaType = MediaType.valueOf(contentType);
boolean visible = VISIBLE_TYPES.stream().anyMatch(visibleType -> visibleType.includes(mediaType));
if (visible) {
try {
String contentString = new String(content, contentEncoding);
Stream.of(contentString.split("\r\n|\r|\n")).forEach(line -> {
try {
printLines(line);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
// log.info("{} {}", prefix, line));
} catch (UnsupportedEncodingException e) {
log.info("{} [{} bytes content]", prefix, content.length);
}
} else {
log.info("{} [{} bytes content]", prefix, content.length);
}
}
private static ContentCachingRequestWrapper wrapRequest(HttpServletRequest request) {
if (request instanceof ContentCachingRequestWrapper) {
return (ContentCachingRequestWrapper) request;
} else {
return new ContentCachingRequestWrapper(request);
}
}
private static ContentCachingResponseWrapper wrapResponse(HttpServletResponse response) {
if (response instanceof ContentCachingResponseWrapper) {
return (ContentCachingResponseWrapper) response;
} else {
return new ContentCachingResponseWrapper(response);
}
}
}
파일의 출력:
127.0.0.1|>
POST
/createUser
127.0.0.1|>
session Id:C0793464532E7F0C7154913CBA018B2B
Request:
{
"name": "asdasdas",
"birthDate": "2018-06-21T17:11:15.679+0000"
}
127.0.0.1|<
200
OK
127.0.0.1|<
Response:
{"name":"asdasdas","birthDate":"2018-06-21T17:11:15.679+0000","id":4}
사용자 정의 스프링 인터셉터를 구성할 수도 있다.HandlerInterceptorAdapter
사전 전용/사후 전용 인터셉터의 단순화된 구현:
@Component
public class CustomHttpInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle (final HttpServletRequest request, final HttpServletResponse response,
final Object handler)
throws Exception {
// Logs here
return super.preHandle(request, response, handler);
}
@Override
public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response,
final Object handler, final Exception ex) {
// Logs here
}
}
그런 다음 원하는 만큼의 인터셉터를 등록하십시오.
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
CustomHttpInterceptor customHttpInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(customHttpInterceptor).addPathPatterns("/endpoints");
}
}
참고: @Robert가 말한 바와 같이, 당신은 의 구체적인 구현에 주목할 필요가 있다.HttpServletRequest
그리고HttpServletResponse
응용 프로그램이 사용 중임.
예를 들어, 를 사용하는 앱의 경우ShallowEtagHeaderFilter
은 , 구현응 은 은 .ContentCachingResponseWrapper
다음 작업을 수행하십시오.
@Component
public class CustomHttpInterceptor extends HandlerInterceptorAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomHttpInterceptor.class);
private static final int MAX_PAYLOAD_LENGTH = 1000;
@Override
public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response,
final Object handler, final Exception ex) {
final byte[] contentAsByteArray = ((ContentCachingResponseWrapper) response).getContentAsByteArray();
LOGGER.info("Request body:\n" + getContentAsString(contentAsByteArray, response.getCharacterEncoding()));
}
private String getContentAsString(byte[] buf, String charsetName) {
if (buf == null || buf.length == 0) {
return "";
}
try {
int length = Math.min(buf.length, MAX_PAYLOAD_LENGTH);
return new String(buf, 0, length, charsetName);
} catch (UnsupportedEncodingException ex) {
return "Unsupported Encoding";
}
}
}
아래에 붙여넣은 코드는 내 테스트와 함께 작동하며, 내 [github 프로젝트][1]에서 다운로드 받을 수 있으며, 생산 프로젝트에 기반한 솔루션을 적용한 후 공유할 수 있다.
@Configuration
public class LoggingFilter extends GenericFilterBean {
/**
* It's important that you actually register your filter this way rather then just annotating it
* as @Component as you need to be able to set for which "DispatcherType"s to enable the filter
* (see point *1*)
*
* @return
*/
@Bean
public FilterRegistrationBean<LoggingFilter> initFilter() {
FilterRegistrationBean<LoggingFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new LoggingFilter());
// *1* make sure you sett all dispatcher types if you want the filter to log upon
registrationBean.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
// *2* this should put your filter above any other filter
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registrationBean;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
ContentCachingRequestWrapper wreq =
new ContentCachingRequestWrapper(
(HttpServletRequest) request);
ContentCachingResponseWrapper wres =
new ContentCachingResponseWrapper(
(HttpServletResponse) response);
try {
// let it be ...
chain.doFilter(wreq, wres);
// makes sure that the input is read (e.g. in 404 it may not be)
while (wreq.getInputStream().read() >= 0);
System.out.printf("=== REQUEST%n%s%n=== end request%n",
new String(wreq.getContentAsByteArray()));
// Do whatever logging you wish here, in this case I'm writing request
// and response to system out which is probably not what you wish to do
System.out.printf("=== RESPONSE%n%s%n=== end response%n",
new String(wres.getContentAsByteArray()));
// this is specific of the "ContentCachingResponseWrapper" we are relying on,
// make sure you call it after you read the content from the response
wres.copyBodyToResponse();
// One more point, in case of redirect this will be called twice! beware to handle that
// somewhat
} catch (Throwable t) {
// Do whatever logging you whish here, too
// here you should also be logging the error!!!
throw t;
}
}
}
만약 누군가가 여전히 여기에 그것을 필요로 한다면, 그것은 Spring HttpTrace Activator를 통한 간단한 구현이다.그러나 그들이 상부에 말했듯이 그것은 시체를 기록하지 않는다.
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.springframework.boot.actuate.trace.http.HttpTrace;
import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository;
import org.springframework.stereotype.Repository;
@Slf4j
@Repository
public class LoggingInMemoryHttpTraceRepository extends InMemoryHttpTraceRepository {
public void add(HttpTrace trace) {
super.add(trace);
log.info("Trace:" + ToStringBuilder.reflectionToString(trace));
log.info("Request:" + ToStringBuilder.reflectionToString(trace.getRequest()));
log.info("Response:" + ToStringBuilder.reflectionToString(trace.getResponse()));
}
}
@han의 대답은 그것이 나에게 효과가 있으려면 약간의 수정이 필요했지만, 그것은 단연코 내가 얻을 수 있는 가장 맞춤화된 것이다.
나한텐 안 먹혔어, 아마 나한텐 핸들러도 있으니까.인터셉터어댑터[?] 하지만 나는 그 버전에 있는 서버로부터 계속 나쁜 응답을 받았다.여기 내가 수정한 것이 있다.
public class LoggableDispatcherServlet extends DispatcherServlet {
private final Log logger = LogFactory.getLog(getClass());
@Override
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
long startTime = System.currentTimeMillis();
try {
super.doDispatch(request, response);
} finally {
log(new ContentCachingRequestWrapper(request), new ContentCachingResponseWrapper(response),
System.currentTimeMillis() - startTime);
}
}
private void log(HttpServletRequest requestToCache, HttpServletResponse responseToCache, long timeTaken) {
int status = responseToCache.getStatus();
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("httpStatus", status);
jsonObject.addProperty("path", requestToCache.getRequestURI());
jsonObject.addProperty("httpMethod", requestToCache.getMethod());
jsonObject.addProperty("timeTakenMs", timeTaken);
jsonObject.addProperty("clientIP", requestToCache.getRemoteAddr());
if (status > 299) {
String requestBody = null;
try {
requestBody = requestToCache.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
} catch (IOException e) {
e.printStackTrace();
}
jsonObject.addProperty("requestBody", requestBody);
jsonObject.addProperty("requestParams", requestToCache.getQueryString());
jsonObject.addProperty("tokenExpiringHeader",
responseToCache.getHeader(ResponseHeaderModifierInterceptor.HEADER_TOKEN_EXPIRING));
}
logger.info(jsonObject);
}
}
초기 질문이 게시된 이후 액추에이터 HTTP 트레이스에 대한 개발이 있었는가? 즉, 응답 본체로 이를 강화할 수 있는 방법이 있는가?
MDC의 사용자 지정 메타데이터나 TrackId 및 spanId와 같은 Spring-Sleuth 또는 Zipkin의 사용자 지정 메타데이터로 강화하면 어떨까?
또한 내게는 액추에이터 HTTP 트레이스가 스프링 부트 2.2.3을 작동하지 않았고, 여기서 해결 방법을 찾았다: https://juplo.de/actuator-httptrace-does-not-work-with-spring-boot-2-2/
pom.xml
<dependency>
<groupId>org.springframework.boot
<artifactId>spring-boot-starter-actuator
</dependency>
application.properties
management.endpoints.web.exposure.include=httptrace
해결 방법:
이 문제에 대한 간단한 해결 방법은 InMemoryHttpTraceRepository 유형의 @Bean을 @Configuration-class에 추가하는 것이다.
@Bean
public HttpTraceRepository htttpTraceRepository()
{
return new InMemoryHttpTraceRepository();
}
설명:
이 문제의 원인은 버그가 아니라 기본 구성의 합법적인 변경이다.불행히도, 이러한 변경은 설명서의 해당 섹션에 기록되지 않는다.대신 Spring Boot용 업그레이드 노트 2.2에서 호출됨
기본 구현은 캡처된 데이터를 메모리에 저장한다.따라서 사용자가 알지 못하는 사이에 메모리가 많이 소모되거나 심지어 필요한 메모리까지 소모된다.이것은 특히 메모리가 귀중한 재화인 클러스터 환경에서 바람직하지 않다.기억하십시오. 스프링 부트는 클러스터 배치를 단순화하기 위해 개발됨!
즉, 이 기능이 현재 기본적으로 설정되어 있고 필요한 경우 사용자가 명시적으로 켜야 하는 이유.
요청 페이로드의 일부만 보이면setMaxPayloadLength
기본적으로 요청 본문에 50자만 표시되므로 기능하십시오.또한, 설정setIncludeHeaders
인증 헤더를 기록하지 않으려면 false를 사용하는 것이 좋다!
@Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
loggingFilter.setIncludeClientInfo(false);
loggingFilter.setIncludeQueryString(false);
loggingFilter.setIncludePayload(true);
loggingFilter.setIncludeHeaders(false);
loggingFilter.setMaxPayloadLength(500);
return loggingFilter;
}
앞서 제안했듯이, 로그북은 이것과 거의 완벽하지만, 나는 Java 모듈을 사용할 때 설정하는데 약간의 어려움이 있었다, 왜냐하면 그 사이에 분할된 패키지가 있기 때문이다.logbook-api
그리고logbook-core
.
Gradle + Spring Boot 프로젝트를 위해서는
빌드.그라들리다
dependencies {
compileOnly group: 'org.zalando', name: 'logbook-api', version: '2.4.1'
runtimeOnly group: 'org.zalando', name: 'logbook-spring-boot-starter', version: '2.4.1'
//...
}
logback-spring.xml
<configuration>
<!-- HTTP Requests and Responses -->
<logger name="org.zalando.logbook" level="trace" />
</configuration>
입력 매개 변수와 본문으로 모든 요청을 기록하기 위해 필터와 인터셉터를 사용할 수 있다.그러나 필터나 인터셉터를 사용하는 동안에는 요청서를 여러 번 인쇄할 수 없다.더 좋은 방법은 우리가 스프링-AOP를 사용할 수 있다는 것이다.이것을 사용함으로써 우리는 기록 메커니즘을 응용 프로그램에서 분리할 수 있다.AOP는 애플리케이션에서 각 방법의 입력과 출력을 로깅하는 데 사용할 수 있다.
제 해결책은:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
@Aspect
@Component
public class LoggingAdvice {
private static final Logger logger =
LoggerFactory.getLogger(LoggingAdvice.class);
//here we can provide any methodName, packageName, className
@Pointcut(value = "execution(* com.package.name.*.*.*(..) )")
public void myPointcut() {
}
@Around("myPointcut()")
public Object applicationLogger(ProceedingJoinPoint pjt) throws Throwable {
ObjectMapper mapper = new ObjectMapper();
String methodName = pjt.getSignature().getName();
String className = pjt.getTarget().getClass().toString();
String inputParams = this.getInputArgs(pjt ,mapper);
logger.info("method invoked from " + className + " : " + methodName + "--Request Payload::::"+inputParams);
Object object = pjt.proceed();
try {
logger.info("Response Object---" + mapper.writeValueAsString(object));
} catch (Exception e) {
}
return object;
}
private String getInputArgs(ProceedingJoinPoint pjt,ObjectMapper mapper) {
Object[] array = pjt.getArgs();
CodeSignature signature = (CodeSignature) pjt.getSignature();
StringBuilder sb = new StringBuilder();
sb.append("{");
int i = 0;
String[] parameterNames = signature.getParameterNames();
int maxArgs = parameterNames.length;
for (String name : signature.getParameterNames()) {
sb.append("[").append(name).append(":");
try {
sb.append(mapper.writeValueAsString(array[i])).append("]");
if(i != maxArgs -1 ) {
sb.append(",");
}
} catch (Exception e) {
sb.append("],");
}
i++;
}
return sb.append("}").toString();
}
}
여기 부팅 앱에서 Tomcat을 사용하면org.apache.catalina.filters.RequestDumperFilter
(그러나 "한곳에서 예외"를 제공하지는 않을 것이다.)
스프링 부트 구성 서버가 구성된 경우 클래스에 대해 디버그 로거를 활성화하십시오.
Http11InputBuffer.Http11InputBuffer.java
디버그가 모든 요청에 대한 모든 요청과 응답을 기록함
참고
@Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
...
}
스프링 보안 필터 체인에서는 접근 방법이 작동하지 않는다.다음과 같이 수동으로 CommonsRequestLoggingFilter를 추가해야 함
protected void configure(HttpSecurity http) throws Exception {
HttpSecurity filter = http
.cors().and().addFilterBefore(new CommonsRequestLoggingFilter(), CorsFilter.class);
}
나는 LoggingConfig.java라는 파일을 다음과 같은 내용으로 만들었다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.CommonsRequestLoggingFilter;
@Configuration
public class LoggingConfig {
@Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
final CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
loggingFilter.setIncludeClientInfo(true);
loggingFilter.setIncludeQueryString(true);
loggingFilter.setIncludePayload(true);
loggingFilter.setMaxPayloadLength(32768);
return loggingFilter;
}
}
application.properties에서 나는 다음과 같이 덧붙였다.
logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG
사용자 정의 형식의 로깅 요청 + 페이로드:
사용자 지정 형식은 Spring logger Bean - org/springframework/web/filter/AbstractRequestLoggingFilter.java의 슈퍼 구현을 재정의하십시오.
GET 요청을 건너뛰고 정보 로그 수준이 있는 쓰기 요청(PUT, Patch, DELETE 등)만 추적한다고 가정합시다.
@Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
CommonsRequestLoggingFilter logFilter = new CommonsRequestLoggingFilter() {
@Override
protected boolean shouldLog(HttpServletRequest request) {
return logger.isInfoEnabled() && !Objects.equals(request.getMethod(), "GET");
}
@Override
protected void beforeRequest(HttpServletRequest request, String message) {
// Do nothing if you need logging payload.
// As, Before the Request, the payload is not read from the input-stream, yet.
}
@Override
protected void afterRequest(HttpServletRequest request, String message) {
logger.info(message); // Or log to a file here, as OP asks.
}
@Override
protected @NonNull String createMessage(HttpServletRequest request, @NonNull String prefix, @NonNull String suffix) {
// Output: [PUT][/api/my-entity], user:[my-loging], payload was:[{ "id": 33, "value": 777.00}]
StringBuilder msg = new StringBuilder()
.append(prefix)
.append("[").append(request.getMethod()).append("]")
.append("[").append(request.getRequestURI()).append("]");
String user = request.getRemoteUser();
msg.append(", user:[").append(null == user ? "" : user).append("]");
String payload = getMessagePayload(request);
if (payload != null) {
// It's not null on After event. As, on Before event, the Input stream was not read, yet.
msg.append(", payload was:[").append(payload.replace("\n", "")).append("]"); // Remove /n to be compliant with elastic search readers.
}
msg.append(suffix);
return msg.toString();
}
};
logFilter.setBeforeMessagePrefix("Incoming REST call: -->>>[");
logFilter.setBeforeMessageSuffix("]...");
logFilter.setAfterMessagePrefix("REST call processed: -<<<[");
logFilter.setAfterMessageSuffix("]");
logFilter.setIncludePayload(true);
logFilter.setMaxPayloadLength(64000);
return logFilter;
}
로그 기록 요청 + 응답/상태:
https://www.baeldung.com/spring-http-logging#custom-request-logging을 참조하십시오.
(정확한 코드 예를 여기에 추가할 수 있다./ 50개 이상의 업보트에 도달하면/
400개만 발생하는 요청을 기록하려면:
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.io.FileUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.AbstractRequestLoggingFilter;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.WebUtils;
/**
* Implementation is partially copied from {@link AbstractRequestLoggingFilter} and modified to output request information only if request resulted in 400.
* Unfortunately {@link AbstractRequestLoggingFilter} is not smart enough to expose {@link HttpServletResponse} value in afterRequest() method.
*/
@Component
public class RequestLoggingFilter extends OncePerRequestFilter {
public static final String DEFAULT_AFTER_MESSAGE_PREFIX = "After request [";
public static final String DEFAULT_AFTER_MESSAGE_SUFFIX = "]";
private final boolean includeQueryString = true;
private final boolean includeClientInfo = true;
private final boolean includeHeaders = true;
private final boolean includePayload = true;
private final int maxPayloadLength = (int) (2 * FileUtils.ONE_MB);
private final String afterMessagePrefix = DEFAULT_AFTER_MESSAGE_PREFIX;
private final String afterMessageSuffix = DEFAULT_AFTER_MESSAGE_SUFFIX;
/**
* The default value is "false" so that the filter may log a "before" message
* at the start of request processing and an "after" message at the end from
* when the last asynchronously dispatched thread is exiting.
*/
@Override
protected boolean shouldNotFilterAsyncDispatch() {
return false;
}
@Override
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain)
throws ServletException, IOException {
final boolean isFirstRequest = !isAsyncDispatch(request);
HttpServletRequest requestToUse = request;
if (includePayload && isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
requestToUse = new ContentCachingRequestWrapper(request, maxPayloadLength);
}
final boolean shouldLog = shouldLog(requestToUse);
try {
filterChain.doFilter(requestToUse, response);
} finally {
if (shouldLog && !isAsyncStarted(requestToUse)) {
afterRequest(requestToUse, response, getAfterMessage(requestToUse));
}
}
}
private String getAfterMessage(final HttpServletRequest request) {
return createMessage(request, this.afterMessagePrefix, this.afterMessageSuffix);
}
private String createMessage(final HttpServletRequest request, final String prefix, final String suffix) {
final StringBuilder msg = new StringBuilder();
msg.append(prefix);
msg.append("uri=").append(request.getRequestURI());
if (includeQueryString) {
final String queryString = request.getQueryString();
if (queryString != null) {
msg.append('?').append(queryString);
}
}
if (includeClientInfo) {
final String client = request.getRemoteAddr();
if (StringUtils.hasLength(client)) {
msg.append(";client=").append(client);
}
final HttpSession session = request.getSession(false);
if (session != null) {
msg.append(";session=").append(session.getId());
}
final String user = request.getRemoteUser();
if (user != null) {
msg.append(";user=").append(user);
}
}
if (includeHeaders) {
msg.append(";headers=").append(new ServletServerHttpRequest(request).getHeaders());
}
if (includeHeaders) {
final ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
if (wrapper != null) {
final byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
final int length = Math.min(buf.length, maxPayloadLength);
String payload;
try {
payload = new String(buf, 0, length, wrapper.getCharacterEncoding());
} catch (final UnsupportedEncodingException ex) {
payload = "[unknown]";
}
msg.append(";payload=").append(payload);
}
}
}
msg.append(suffix);
return msg.toString();
}
private boolean shouldLog(final HttpServletRequest request) {
return true;
}
private void afterRequest(final HttpServletRequest request, final HttpServletResponse response, final String message) {
if (response.getStatus() == HttpStatus.BAD_REQUEST.value()) {
logger.warn(message);
}
}
}
당신은 단 하나의 PLCae에서 이 모든 것을 처리하기 위해 측면 지향 프로그래밍을 사용할 수 있다.
'programing' 카테고리의 다른 글
vuejs의 json 개체에서 중첩된 키를 찾는 방법 (0) | 2022.05.16 |
---|---|
"|="는 무슨 뜻인가?(파이프 이퀄 오퍼레이터) (0) | 2022.05.16 |
파이프 문자("|")로 문자열 분할 (0) | 2022.05.16 |
Vuejs 렌더링 기능 내에서 슬롯을 복제하는 방법 (0) | 2022.05.16 |
현재 날짜와 시간을 UTC 또는 자바 GMT로 받는 방법은? (0) | 2022.05.16 |