Spring WS MTOM : change boundary name - java

Spring ws with mtom is forcing the "Content-Type" header of the response to :
Multipart/Related; boundary="----=_Part_9_1258512392.1655296193519"; type="application/xop+xml"; start-info="text/xml"
The issue here is I have to force the boundary value to
--uuid:something
I am using a ClientInterceptor class to override the Content-Type Header :
public class RequestHandler implements EndpointInterceptor {
#Override
public boolean handleRequest(MessageContext messageContext, Object endpoint) {
return true;
}
#Override
public boolean handleResponse(MessageContext messageContext, Object endpoint) {
this.setMTOM(messageContext);
return true;
}
#Override
public boolean handleFault(MessageContext messageContext, Object endpoint) {
return true;
}
#Override
public void afterCompletion(MessageContext messageContext, Object endpoint, Exception ex) {
}
private void setMTOM(MessageContext messageContext) {
String uri;
try {
uri = TransportContextHolder.getTransportContext().getConnection().getUri().toString();
} catch (URISyntaxException e) {
throw new LocalizedStatusException(INTERNAL_SERVER_ERROR, "messages.error_parsing_uri");
}
if (!uri.contains("no-mtom")) {
String headerUUID = UUID.randomUUID().toString().strip();
SaajSoapMessage response = (SaajSoapMessage) messageContext.getResponse();
response.getSaajMessage().getSOAPPart().setContentId("<rootpart*" + headerUUID + "#example.jaxws.sun.com>");
response.getSaajMessage().getMimeHeaders().setHeader(
HEADER_CONTENT_TYPE,
"Multipart/Related; boundary=\"uuid:" + headerUUID + "\"; start=\"<rootpart*" + headerUUID + "#example.jaxws.sun.com>\"; type=\"application/xop+xml\"; start-info=\"text/xml\""
);
response.getSaajMessage().getSOAPPart().setMimeHeader(
HEADER_CONTENT_TRANSFER_ENCODING,
"binary"
);
}
}
}
But the SaajSoapMessage object have a "saveChanges()" method which is called after the interceptor.
This method is overriding this Content-Type header.
Is there any way to force this header ?
Thank you in advance, and sorry for my poor english.

Related

How to get request payload class type and response body class type in RestTemplateInterceptor to mask sensitive information using custom annotation?

In my spring boot application I've used a RestTemplateInterceptor to log request and response details in debug mode. To mask the sensitive information in request payload and response body, I've created a custom annotation #LogMaskedStringValue and annotated some fields in request DTO and response DTO. I've created a Serializer MaskStringSerializer to mask the annotated fields with the help of object mapper.
I tried to set the request payload type and expected response body type in request headers and I'm retrieving it in interceptor. But it is not the legitimate way to do, cause the header dependency prevents to use this interceptor in other applications, I tried using RestTemplateRequestCustomizer , Unfortunately it didn't work. Is there any way to get the request payload type and response body type in RestTemplateInterceptor ?
```
#Slf4j
public class RestTemplateLoggingInterceptor implements ClientHttpRequestInterceptor {
private final LogDetailsStorage logDetailsStorage;
private final static ObjectMapper objectMapper = new ObjectMapper();
static {
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
public RestTemplateLoggingInterceptor(LogDetailsStorage logDetailsStorage, String message) {
this.logDetailsStorage = logDetailsStorage;
this.message = message;
}
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
logDetailsStorage.setOutboundStartTime(System.currentTimeMillis());
String requestType = request.getHeaders().getFirst("requestType");
String responseType = request.getHeaders().getFirst("responseType");
request.getHeaders().remove("requestType");
request.getHeaders().remove("responseType");
logRequest(request, body, requestType);
ClientHttpResponse response = execution.execute(request, body);
logResponse(response, responseType);
return response;
}
private void logRequest(HttpRequest request, byte[] body, String requestType) {
if (log.isDebugEnabled()) {
logDetailsStorage.setOutboundRequestUrl(request.getURI().toString());
logDetailsStorage.setOutboundRequestMethod(request.getMethodValue());
MDC.put(MdcKey.OUTBOUND_REQUEST_METHOD.getMdcKey(), logDetailsStorage.getOutboundRequestMethod());
MDC.put(MdcKey.OUTBOUND_REQUEST_URL.getMdcKey(), logDetailsStorage.getOutboundRequestUrl());
if (body != null && body.length > 0) {
String requestPayload = new String(body, StandardCharsets.UTF_8);
logDetailsStorage.setOutboundRequestPayload(getMaskedPayload(requestType, requestPayload));
MDC.put(MdcKey.OUTBOUND_REQUEST_PAYLOAD.getMdcKey(), logDetailsStorage.getOutboundRequestPayload());
}
log.debug("Making request for " + logDetailsStorage.getOutboundRequestUrl());
MDC.remove(MdcKey.OUTBOUND_REQUEST_METHOD.getMdcKey());
MDC.remove(MdcKey.OUTBOUND_REQUEST_URL.getMdcKey());
MDC.remove(MdcKey.OUTBOUND_REQUEST_PAYLOAD.getMdcKey());
}
}
private void logResponse(ClientHttpResponse response, String responseType) throws IOException {
if (log.isDebugEnabled()) {
String responsePayload = StreamUtils.copyToString(response.getBody(), Charset.defaultCharset());
logDetailsStorage.setOutboundResponsePayload(getMaskedPayload(responseType, responsePayload));
logDetailsStorage.setOutboundStatusCode(String.valueOf(response.getRawStatusCode()));
logDetailsStorage.setOutboundExecutionTime((System.currentTimeMillis() - logDetailsStorage.getOutboundStartTime()) / 1000d + " seconds");
MDC.put(MdcKey.OUTBOUND_REQUEST_METHOD.getMdcKey(), logDetailsStorage.getOutboundRequestMethod());
MDC.put(MdcKey.OUTBOUND_REQUEST_URL.getMdcKey(), logDetailsStorage.getOutboundRequestUrl());
MDC.put(MdcKey.OUTBOUND_RESPONSE_PAYLOAD.getMdcKey(), logDetailsStorage.getOutboundResponsePayload());
MDC.put(MdcKey.OUTBOUND_STATUS_CODE.getMdcKey(), logDetailsStorage.getOutboundStatusCode());
if (logDetailsStorage.getOutboundRequestPayload() != null) {
MDC.put(MdcKey.OUTBOUND_REQUEST_PAYLOAD.getMdcKey(), logDetailsStorage.getOutboundRequestPayload());
}
MDC.put(MdcKey.OUTBOUND_EXECUTION_TIME.getMdcKey(), logDetailsStorage.getOutboundExecutionTime());
log.debug("Got Response for "+ logDetailsStorage.getOutboundRequestUrl());
MDC.remove(MdcKey.OUTBOUND_REQUEST_METHOD.getMdcKey());
MDC.remove(MdcKey.OUTBOUND_REQUEST_URL.getMdcKey());
MDC.remove(MdcKey.OUTBOUND_REQUEST_PAYLOAD.getMdcKey());
MDC.remove(MdcKey.OUTBOUND_EXECUTION_TIME.getMdcKey());
MDC.remove(MdcKey.OUTBOUND_STATUS_CODE.getMdcKey());
MDC.remove(MdcKey.OUTBOUND_RESPONSE_PAYLOAD.getMdcKey());
}
}
private String getMaskedPayload(String classType, String payload) {
if (!StringUtils.isEmpty(classType)) {
try {
Object obj = objectMapper.readValue(payload, Class.forName(classType));
payload = LogUtil.getObjectAsMaskedJsonString(obj);
} catch (JsonProcessingException e) {
log.error("'Failed to parse the payload : {}", payload, e);
} catch (ClassNotFoundException e) {
log.error("Class not found exception occurred : {}", classType, e);
}
}
else {
log.warn("ClassType is empty during getMaskedPayload : {}", classType);
}
return payload;
}
}
```
```
public class MaskStringSerializer extends StdSerializer<String> implements ContextualSerializer {
private String mask;
public MaskStringSerializer() {
super(String.class);
}
public MaskStringSerializer(String mask) {
super(String.class);
this.mask = mask;
}
#Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
Optional<String> maskValue = Optional.ofNullable(property)
.map(p -> p.getAnnotation(LogMaskStringValue.class))
.map(LogMaskStringValue::value);
return maskValue.map(MaskStringSerializer::new).orElseGet(MaskStringSerializer::new);
}
#Override
public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
if (mask != null) {
gen.writeString(mask);
} else {
gen.writeString(Optional.ofNullable(value).orElse("null"));
}
}
}
```
```
#UtilityClass
#Slf4j
public class LogUtil {
private final static ObjectMapper sensitiveMapper = new ObjectMapper();
static {
SimpleModule module = new SimpleModule();
module.addSerializer(new MaskStringSerializer());
sensitiveMapper.registerModule(module);
}
public static String getObjectAsMaskedJsonString(Object object) {
String requestBody;
try {
requestBody = sensitiveMapper.writeValueAsString(object);
} catch (JsonProcessingException jsonProcessingException) {
log.error("Error while parsing object: {}", object, jsonProcessingException);
requestBody = object.toString();
}
return requestBody;
}
}
```
```
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class Card {
#LogMaskStringValue
private String id;
private String type;
private String last4;
private Integer expirationMonth;
private Integer expirationYear;
}
```
```
`

#RequestBody not able to convert object derived from AES Encrypted String

From client side am passing an AES encrypted String with Content Type text/plain.
The AES encrypted String is Decrypted before reaching the controller through a Filter.
CustomEncryptedFilter
#Component
#Order(0)
public class CustomEncryptedFilter implements Filter {
private static final Logger logger = LogManager.getLogger(CustomEncryptedFilter.class.getName());
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
logger.info("************** Encryption Filter - START ***********************");
String encryptedString = IOUtils.toString(request.getInputStream());
if (encryptedString != null && encryptedString.length() > 0) {
byte[] decryptedString = new AESEncrytion().decrypt(encryptedString).getBytes();
if (request instanceof HttpServletRequest) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
CustomHttpServletRequestWrapper requestWrapper
= new CustomHttpServletRequestWrapper(httpServletRequest,decryptedString);
logger.info("Content Type: {}", requestWrapper.getContentType());
logger.info("Request Body: {}", IOUtils.toString(requestWrapper.getInputStream()));
chain.doFilter(requestWrapper, response);
} else {
chain.doFilter(request, response);
}
} else {
logger.info("Request is Invalid or Empty");
chain.doFilter(request, response);
}
}
}
Here I will getting the current request body which is an AES encrypted String
then am decrypting it to convert into a String.
encrypted String - Ijwmn5sZ5HqoUPb15c5idjxetqmC8Sln6+d2BPaYzxA=
Original String - {"username":"thivanka"}
After getting the decrypted String (Json object) i am appending it to the request body
by extending HttpServletRequestWrapper
public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
private static final Logger logger = LogManager.getLogger(CustomHttpServletRequestWrapper.class.getName());
private ByteArrayInputStream requestBody;
public CustomHttpServletRequestWrapper(HttpServletRequest request, byte[] decryptedString) {
super(request);
try {
requestBody = new ByteArrayInputStream(decryptedString);
} catch (Exception e) {
logger.error(e);
e.printStackTrace();
}
}
#Override
public String getHeader(String headerName) {
String headerValue = super.getHeader(headerName);
if ("Accept".equalsIgnoreCase(headerName)) {
return headerValue.replaceAll(MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_VALUE);
} else if ("Content-Type".equalsIgnoreCase(headerName)) {
return headerValue.replaceAll(MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_VALUE);
}
return headerValue;
}
#SuppressWarnings("unchecked")
#Override
public Enumeration getHeaderNames() {
HttpServletRequest request = (HttpServletRequest) getRequest();
List list = new ArrayList();
Enumeration e = request.getHeaderNames();
while (e.hasMoreElements()) {
String headerName = (String) e.nextElement();
String headerValue = request.getHeader(headerName);
if ("Accept".equalsIgnoreCase(headerName)) {
headerValue.replaceAll(MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_VALUE);
} else if ("Content-Type".equalsIgnoreCase(headerName)) {
headerValue.replaceAll(MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_VALUE);
}
list.add(headerName);
}
return Collections.enumeration(list);
}
#SuppressWarnings("unchecked")
#Override
public Enumeration getHeaders(final String headerName) {
HttpServletRequest request = (HttpServletRequest) getRequest();
List list = new ArrayList();
Enumeration e = request.getHeaders(headerName);
while (e.hasMoreElements()) {
String header = e.nextElement().toString();
if (header.equalsIgnoreCase(MediaType.TEXT_PLAIN_VALUE)) {
header = MediaType.APPLICATION_JSON_VALUE;
}
list.add(header);
}
return Collections.enumeration(list);
}
#Override
public String getContentType() {
String contentTypeValue = super.getContentType();
if (MediaType.TEXT_PLAIN_VALUE.equalsIgnoreCase(contentTypeValue)) {
return MediaType.APPLICATION_JSON_VALUE;
}
return contentTypeValue;
}
#Override
public BufferedReader getReader() throws UnsupportedEncodingException {
return new BufferedReader(new InputStreamReader(requestBody, "UTF-8"));
}
#Override
public ServletInputStream getInputStream() throws IOException {
return new ServletInputStream() {
#Override
public int read() {
return requestBody.read();
}
#Override
public boolean isFinished() {
// TODO Auto-generated method stub
return false;
}
#Override
public boolean isReady() {
// TODO Auto-generated method stub
return false;
}
#Override
public void setReadListener(ReadListener listener) {
// TODO Auto-generated method stub
}
};
}
}
Apart from adding the new request body am also changing the MediaType from text/plain
to application/json in order for my #RequestBody annotation to pick up the media type and
perform object conversion.
Here's my Controller
#CrossOrigin(origins = "*", allowedHeaders = "*")
#RestController
#RequestMapping("/api/mobc")
public class HomeController {
private static final Logger logger = LogManager.getLogger(HomeController.class.getName());
#RequestMapping(value="/hello", method=RequestMethod.POST,consumes="application/json", produces="application/json")
public ResponseEntity<?> Message(#RequestBody LoginForm loginForm,HttpServletRequest request) {
logger.info("In Home Controller");
logger.info("Content Type: {}", request.getContentType());
return ResponseEntity.status(HttpStatus.OK).body(loginForm);
}
}
LoginForm Object (I removed the Getters/Setters for readability)
public class LoginForm {
private String username;
private String password;
}
Unfortunately am getting the error. What am i doing wrong here.
ExceptionHandlerExceptionResolver - Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing
Possible issue
I suppose that IOUtils.toString(InputStream stream) reads all bytes from the InputStream. But InputStream could be read only once.
Your logging statement
logger.info("Request Body: {}", IOUtils.toString(requestWrapper.getInputStream()));
Reads an InputStream, so Spring can't read it a second time. Try replacing IOUtils.toString(requestWrapper.getInputStream()) with new String(encryptedString, Charset.defaultCharset()).
Other implementation proposal
You can implement custom RequestBodyAdvice which will decrypt the message and change headers if needed.
As from Spring's JavaDoc:
Implementations of this contract may be registered directly with the RequestMappingHandlerAdapter or more likely annotated with #ControllerAdvice in which case they are auto-detected.
Here is an example implementation of advice that changes the first byte of a message to { and last byte to }. Your implementation can modify the message decrypting it.
#ControllerAdvice
class CustomRequestBodyAdvice extends RequestBodyAdviceAdapter {
#Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
try (InputStream inputStream = inputMessage.getBody()) {
byte[] bytes = inputStream.readAllBytes();
bytes[0] = 0x7b; // 0x7b = '{'
bytes[bytes.length - 1] = 0x7d; // 0x7d = '}'
return new CustomMessage(new ByteArrayInputStream(bytes), inputMessage.getHeaders());
}
}
}
class CustomMessage implements HttpInputMessage {
private final InputStream body;
private final HttpHeaders httpHeaders;
public CustomMessage(InputStream body, HttpHeaders httpHeaders) {
this.body = body;
this.httpHeaders = httpHeaders;
}
#Override
public InputStream getBody() throws IOException {
return this.body;
}
#Override
public HttpHeaders getHeaders() {
return this.httpHeaders;
}
}
Also, there is supports method that returns whether this RequestBodyAdvice should be called. In this example this method always returns true, but you can create custom annotation and check for its existence.
// custom annotation
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#interface AesEncrypted {}
// class: CustomRequestBodyAdvice
#Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasParameterAnnotation(AesEncrypted.class);
}
// controller
#PostMapping("one")
String getDecrypted(#AesEncrypted #RequestBody Data data) {
return data.value;
}
If anyone is struggling with this then the answer is to move to a ContentCachingRequestWrapper. Other approach would be to use the aspect oriented variation suggested by #geobreze which solves the same question.
I just had to modify my HttpServletRequestWrapper to facilitate the change.
Refs -> https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times
This class caches the request body by consuming the InputStream. If we
read the InputStream in one of the filters, then other subsequent
filters in the filter chain can't read it anymore. Because of this
limitation, this class is not suitable in all situations.

How to read request body in HandlerInterceptor?

I have Spring Boot and I need to log user action in DB, so I wrote HandlerInterceptor:
#Component
public class LogInterceptor implements HandlerInterceptor {
#Autovired
private LogUserActionService logUserActionService;
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws IOException {
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
String url = request.getRequestURI();
String queryString = request.getQueryString() != null ? request.getQueryString() : "";
String body = "POST".equalsIgnoreCase(request.getMethod()) ? new BufferedReader(new InputStreamReader(request.getInputStream())).lines().collect(Collectors.joining(System.lineSeparator())) : queryString;
logUserActionService.logUserAction(userName, url, body);
return true;
}
}
But according to this answer Get RequestBody and ResponseBody at HandlerInterceptor "RequestBody can be read only once", so as I understand I read input stream and then Spring tries to do same, but stream has been read already and I'm getting an error: "Required request body is missing ..."
So I tried different ways to make buffered input stream i.e.:
HttpServletRequest httpServletRequest = new ContentCachingRequestWrapper(request);
new BufferedReader(new InputStreamReader(httpServletRequest.getInputStream())).lines().collect(Collectors.joining(System.lineSeparator()))
Or
InputStream bufferedInputStream = new BufferedInputStream(request.getInputStream());
But nothing helped
Also I tried to use
#ControllerAdvice
public class UserActionRequestBodyAdviceAdapter extends RequestBodyAdviceAdapter {
But it has only body, no request info like URL or Request parameters
Also tried to use Filters, but result same.
So I need a good way to get information from request like user, URL, parameters, body (if present) and write it to DB.
To log HTTP Request & Response, you can use RequestBodyAdviceAdapter and ResponseBodyAdvice. here, it is using in my way.
CustomRequestBodyAdviceAdapter.java
#ControllerAdvice
public class CustomRequestBodyAdviceAdapter extends RequestBodyAdviceAdapter {
#Autowired
HttpServletRequest httpServletRequest;
#Override
public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
#Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
// here you can full log httpServletRequest and body.
return super.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
}
}
CustomResponseBodyAdviceAdapter.java
#ControllerAdvice
public class CustomResponseBodyAdviceAdapter implements ResponseBodyAdvice<Object> {
#Autowired
private LoggingService loggingService;
#Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
#Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType,
Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if (serverHttpRequest instanceof ServletServerHttpRequest && serverHttpResponse instanceof ServletServerHttpResponse) {
// here you can full log httpServletRequest and body.
}
return o;
}
}
Above AdviceAdapter cannot handle the GET request. So, you can use HandlerInterceptor.
CustomWebConfigurerAdapter.java
#Component
public class CustomWebConfigurerAdapter implements WebMvcConfigurer {
#Autowired
private CustomLogInterceptor httpServiceInterceptor;
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(httpServiceInterceptor);
}
}
CustomLogInterceptor.java
#Component
public class CustomLogInterceptor implements HandlerInterceptor {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (DispatcherType.REQUEST.name().equals(request.getDispatcherType().name()) && request.getMethod().equals(HttpMethod.GET.name())) {
// here you can full log httpServletRequest and body for GET Request.
}
return true;
}
}
Here you can reference full source code in my git.
springboot-http-request-response-loging-with-json-logger
+Feature => It is already have Integration with ELK (Elasticsearch, Logstash, Kibana)
You can use Filter to log request body.
public class LoggingFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
try {
chain.doFilter(wrappedRequest, res);
} finally {
logRequestBody(wrappedRequest);
}
}
private static void logRequestBody(ContentCachingRequestWrapper request) {
byte[] buf = request.getContentAsByteArray();
if (buf.length > 0) {
try {
String requestBody = new String(buf, 0, buf.length, request.getCharacterEncoding());
System.out.println(requestBody);
} catch (Exception e) {
System.out.println("error in reading request body");
}
}
}
}
The main thing to note here is that you have to pass object of ContentCachingRequestWrapper in filter chain otherwise you won't get request content in it.
In above example, if you use chain.doFilter(req, res) or chain.doFilter(request, res) then you won't get request body in wrappedRequest object.
You can get the Request Body data using RequestBodyAdviceAdapter for POST/PUT requests. You can use HandlerInterceptorAdapter for GET calls. Here's a working example -
https://frandorado.github.io/spring/2018/11/15/log-request-response-with-body-spring.html
#ControllerAdvice
public class CustomRequestBodyAdviceAdapter extends RequestBodyAdviceAdapter
{
#Autowired
HttpServletRequest httpServletRequest;
private static final Log LOGGER = LogFactory.getLog(CustomRequestBodyAdviceAdapter.class);
private static final Charset DEFAULT_CHARSET = ISO_8859_1;
#Override
public boolean supports(MethodParameter methodParameter, Type type,
Class<? extends HttpMessageConverter<?>> aClass)
{
return true;
}
#Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage,
MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType)
{
Instant startTime = Instant.now();
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("REQUEST call Starts :: Start Time : %s ").append(startTime);
try
{
logRequest(httpServletRequest, body);
}
catch (IOException e)
{
LOGGER.info("Exception getting the Request Body into the Log: {}" + e.getMessage());
}
public void logRequest(HttpServletRequest httpServletRequest, Object body) throws IOException
{
StringBuilder stringBuilder = new StringBuilder();
Map<String, String> parameters = buildParametersMap(httpServletRequest);
stringBuilder.append("REQUEST ");
stringBuilder.append("method=[").append(httpServletRequest.getMethod()).append("] ");
stringBuilder.append("path=[").append(httpServletRequest.getRequestURI()).append("] ");
stringBuilder.append("headers=[").append(buildHeadersMap(httpServletRequest)).append("] ");
if (!parameters.isEmpty())
{
stringBuilder.append("parameters=[").append(parameters).append("] ");
}
if (body != null)
{
stringBuilder.append("body=[" + body + "]");
}
ObjectMapper objectMapper = new ObjectMapper();
String jsonInString = null;
try
{
jsonInString = objectMapper.writer().writeValueAsString(body);
}
catch (JsonProcessingException e)
{
throw new RestApiException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
}
stringBuilder.append("REQUEST Body = [").append(jsonInString).append("] ");
LOGGER.info("BODY DATA >>>> " + jsonInString);
LOGGER.info("Body - : {}" + stringBuilder);
}
private Map<String, String> buildParametersMap(HttpServletRequest httpServletRequest)
{
Map<String, String> resultMap = new HashMap<>();
Enumeration<String> parameterNames = httpServletRequest.getParameterNames();
while (parameterNames.hasMoreElements())
{
String key = parameterNames.nextElement();
String value = httpServletRequest.getParameter(key);
resultMap.put(key, value);
}
return resultMap;
}
private Map<String, String> buildHeadersMap(HttpServletRequest request)
{
Map<String, String> map = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements())
{
String key = headerNames.nextElement();
String value = request.getHeader(key);
map.put(key, value);
}
return map;
}
}
I have used ObjectMapper here because I need the body response as raw JSON object, but the afterBodyRead() is invoked after the body is transformed to Java Object.
I found this solved my problem for copying the request buffer for application/json content types. It also shows how to extend the wrapper as the comments to Harshit solution mentions.
https://levelup.gitconnected.com/how-to-log-the-request-body-in-a-spring-boot-application-10083b70c66
The important pieces are that you need a filter to pass along the new request to the server.
#Component
public class LoggingFilter implements Filter {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
if (Arrays.asList("POST", "PUT").contains(httpRequest.getMethod())) {
CustomHttpRequestWrapper requestWrapper = new CustomHttpRequestWrapper(httpRequest);
requestWrapper.setAttribute("input", requestWrapper.getBodyInStringFormat());
filterChain.doFilter(requestWrapper, servletResponse);
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
The logger requires a custom wrapper, and the one spring boot provides seems to be insufficient for application/json type messages.
public class CustomHttpRequestWrapper extends HttpServletRequestWrapper {
public String getBodyInStringFormat() {
return bodyInStringFormat;
}
private final String bodyInStringFormat;
public CustomHttpRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
bodyInStringFormat = readInputStreamInStringFormat(request.getInputStream(), Charset.forName(request.getCharacterEncoding()));
}
private String readInputStreamInStringFormat(InputStream stream, Charset charset) throws IOException {
return getString(stream, charset);
}
static String getString(InputStream stream, Charset charset) throws IOException {
final int MAX_BODY_SIZE = 1024;
final StringBuilder bodyStringBuilder = new StringBuilder();
if (!stream.markSupported()) {
stream = new BufferedInputStream(stream);
}
stream.mark(MAX_BODY_SIZE + 1);
final byte[] entity = new byte[MAX_BODY_SIZE + 1];
final int bytesRead = stream.read(entity);
if (bytesRead != -1) {
bodyStringBuilder.append(new String(entity, 0, Math.min(bytesRead, MAX_BODY_SIZE), charset));
if (bytesRead > MAX_BODY_SIZE) {
bodyStringBuilder.append("...");
}
}
stream.reset();
return bodyStringBuilder.toString();
}
#Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
#Override
public ServletInputStream getInputStream () {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bodyInStringFormat.getBytes());
return new ServletInputStream() {
private boolean finished = false;
#Override
public boolean isFinished() {
return finished;
}
#Override
public int available() {
return byteArrayInputStream.available();
}
#Override
public void close() throws IOException {
super.close();
byteArrayInputStream.close();
}
#Override
public boolean isReady() {
return true;
}
#Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
public int read () {
int data = byteArrayInputStream.read();
if (data == -1) {
finished = true;
}
return data;
}
};
}
}

Catching exception while converting HTTP request data into enum

I have a problem while deserialization of HTTP request value into enum with custom com.fasterxml.jackson.databind.JsonDeserializer deserializer implementation:
public class EnvelopeColorJsonDeserializer extends JsonDeserializer<EnvelopeColor> {
#Override
public EnvelopeColor deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String val = p.getValueAsString();
return EnvelopeColor.fromCode(val);
}
}
This is how I convert value into enum:
public static EnvelopeColor fromCode(String code) {
Assert.notNull(code, "code");
for (EnvelopeColor type : values()) {
if (code.equals(type.code)) {
return type;
}
}
throw new RuntimeException("Not supported color: " + code);
}
Endpoint:
#PostMapping("/")
public #ResponseBody
ResponseEntity add(#RequestBody EnvelopeDto envelope) {
// some stuff goes here...
}
Question
Is there some way how to a) check if HTTP request value is valid enum constant value before going to deserialization process or b) how to catch exception in #ControllerAdvice exception handler? (I would like to avoid throwing custom exception in fromCode() method).
You can add an exception handler for class HttpMessageNotReadableException.
This exception is thrown when Spring is not able to deserialize the payload into the DTO.
#ExceptionHandler(org.springframework.http.converter.HttpMessageNotReadableException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
protected #ResponseBody handleIncorrectData(HttpMessageNotReadableException ex,
HttpServletRequest request, HttpServletResponse response){
....
}
Moreover you can define a custom EnumConverter which will give the exact details to user which are the correct enum values.
public class CustomEnumConverter extends EnumConverter {
#Override
public Object unmarshal(HierarchicalStreamReader reader,
UnmarshallingContext context) {
try {
return super.unmarshal(reader, context);
} catch (IllegalArgumentException e) {
String inputValue = reader.getValue();
Class contextType = context.getRequiredType();
StringBuilder sb = new StringBuilder();
Object[] enumConstants = contextType.getEnumConstants();
for (Object o : enumConstants) {
sb.append(o + ",");
}
if (sb.length() > 0)
sb.deleteCharAt(sb.length() - 1);
throw new InvalidArgumentException(ErrorCode.INVALID_ARGUMENT, inputValue,
reader.getNodeName(), sb.toString());
}
}
}
for catch exception in #ControllerAdvice exception handler, try this :
#ControllerAdvice
#Order(Ordered.HIGHEST_PRECEDENCE)
public class ApplicationExceptionHandler {
private static Logger logger = LoggerFactory.getLogger(ApplicationExceptionHandler.class);
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler({RuntimeException.class})
public #ResponseBody Response<Error> handleIllegalArgumentException(Exception e) {
logger.error("BadRequestException :: ", e);
return new Response<Error>(new Error("BadRequestException", e.getMessage()));
}
}

Jhipster logging [duplicate]

I'm working on REST API with spring boot. I need to log all requests with input params (with methods, eg. GET, POST, etc.), request path, query string, corresponding class method of this request, also response of this action, both success and errors. For example:
Successful request:
http://example.com/api/users/1
Log should look something like this:
{
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: []
}
Or request with error:
http://example.com/api/users/9999
Log should be something like this:
{
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: ...................
]
}
I want Request/Response to be a single entity, with custom information related to this entity, both in successful and error cases.
What is best practice in spring to achieve this, may be with filters? if yes, can you provide concrete example?
I've played with #ControllerAdvice and #ExceptionHandler, but as I mentioned, I need to handle all success and error requests in single place (and single log).
Don't write any Interceptors, Filters, Components, Aspects, etc., this is a very common problem and has been solved many times over.
Spring Boot has a modules called Actuator, which provides HTTP request logging out of the box. There's an endpoint mapped to /trace (SB1.x) or /actuator/httptrace (SB2.0+) which will show you last 100 HTTP requests. You can customize it to log each request, or write to a DB.
To get the endpoints you want, you'll need the spring-boot-starter-actuator dependency, and also to "whitelist" the endpoints you're looking for, and possibly setup or disable security for it.
Also, where will this application run? Will you be using a PaaS? Hosting providers, Heroku for example, provide request logging as part of their service and you don't need to do any coding whatsoever then.
Spring already provides a filter that does this job. Add following bean to your config
#Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
loggingFilter.setIncludeClientInfo(true);
loggingFilter.setIncludeQueryString(true);
loggingFilter.setIncludePayload(true);
loggingFilter.setMaxPayloadLength(64000);
return loggingFilter;
}
Don't forget to change log level of org.springframework.web.filter.CommonsRequestLoggingFilter to DEBUG.
You could use javax.servlet.Filter if there wasn't a requirement to log java method that been executed.
But with this requirement you have to access information stored in handlerMapping of DispatcherServlet. That said, you can override DispatcherServlet to accomplish logging of request/response pair.
Below is an example of idea that can be further enhanced and adopted to your needs.
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 - contains the information about request handler.
You then can register this dispatcher as following:
#Bean
public ServletRegistrationBean dispatcherRegistration() {
return new ServletRegistrationBean(dispatcherServlet());
}
#Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet() {
return new LoggableDispatcherServlet();
}
And here's the sample of logs:
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"}'}
UPDATE
In case of errors Spring does automatic error handling. Therefore, BasicErrorController#error is shown as request handler. If you want to preserve original request handler, then you can override this behavior at spring-webmvc-4.2.5.RELEASE-sources.jar!/org/springframework/web/servlet/DispatcherServlet.java:971 before #processDispatchResult is called, to cache original handler.
The Logbook library is specifically made for logging HTTP requests and responses. It supports Spring Boot using a special starter library.
To enable logging in Spring Boot all you need to do is adding the library to your project's dependencies. For example assuming you are using Maven:
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-spring-boot-starter</artifactId>
<version>1.5.0</version>
</dependency>
By default the logging output looks like this:
{
"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"
}
It does however not output the class name that is handling the request. The library does have some interfaces for writing custom loggers.
Notes
In the meantime the library has significantly evolved, current version is 2.4.1, see https://github.com/zalando/logbook/releases. E.g. the default ouput format has changed, and can be configured, filtered, etc.
Do NOT forget to set the log level to TRACE, else you won't see anything:
logging:
level:
org.zalando.logbook: TRACE
I had defined logging level in application.properties to print requests/responses, method url in the log file
logging.level.org.springframework.web=DEBUG
logging.level.org.hibernate.SQL=INFO
logging.file=D:/log/myapp.log
I had used Spring Boot.
Here is how I do it in spring data rest
by using
org.springframework.web.util.ContentCachingRequestWrapper and
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
}
}
This code works for me in a Spring Boot application - just register it as a filter
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();
}
}
}
If you dont mind trying Spring AOP, this is something I have been exploring for logging purposes and it works pretty well for me. It wont log requests that have not been defined and failed request attempts though.
Add these three dependencies
spring-aop, aspectjrt, aspectjweaver
Add this to your xml config file <aop:aspectj-autoproxy/>
Create an annotation which can be used as a pointcut
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.METHOD,ElementType.TYPE})
public #interface EnableLogging {
ActionType actionType();
}
Now annotate all your rest API methods which you want to log
#EnableLogging(actionType = ActionType.SOME_EMPLOYEE_ACTION)
#Override
public Response getEmployees(RequestDto req, final String param) {
...
}
Now on to the Aspect. component-scan the package which this class is in.
#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 advice runs when a matched method execution returns
normally.
#AfterThrowing advice runs when a matched method execution exits by
throwing an exception.
If you want to read in detail read through this.
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html
Currently Spring Boot has the Actuator feature to get the logs of requests and responses.
But you can also get the logs using Aspect(AOP).
Aspect provides you with annotations like: #Before, #AfterReturning, #AfterThrowing etc.
#Before logs the request, #AfterReturning logs the response and #AfterThrowing logs the error message,
You may not need all endpoints' log, so you can apply some filters on the packages.
Here are some examples:
For Request:
#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) {
}
}
}
Here #Before("within(your.package.where.endpoints.are..*)") has the package path. All endpoints within this package will generate the log.
For Response:
#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");
}
}
Here #AfterReturning("within(your.package.where.endpoints.are..*)") has the package path. All endpoints within this package will generate the log. Also Object returnValue contains the response.
For Exception:
#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());
}
}
Here #AfterThrowing(pointcut = ("within(your.package.where.endpoints.are..*)"), throwing = "e") has the package path. All endpoints within this package will generate the log. Also Exception e contains the error response.
Here is the full code:
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());
}
}
}
Here, using #ConditionalOnExpression("${endpoint.aspect.enabled:true}") you can enable/disable the log. just add endpoint.aspect.enabled:true into the application.property and control the log
More info about AOP visit here:
Spring docs about AOP
Sample article about AOP
After adding Actuators to the spring boot bassed application you have /trace endpoint available with latest requests informations. This endpoint is working based on TraceRepository and default implementation is InMemoryTraceRepository that saves last 100 calls. You can change this by implementing this interface by yourself and make it available as a Spring bean. For example to log all requests to log (and still use default implementation as a basic storage for serving info on /trace endpoint) I'm using this kind of implementation:
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);
}
}
This traceInfo map contains basic informations about request and response in this kind of form:
{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}}}. There is NO response content here.
EDIT! Logging POST data
You can access POST data by overriding WebRequestTraceFilter, but don't think it is a good idea (e.g. all uploaded file content will go to logs)
Here is sample code, but don't use it:
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;
}
}
Please refer to below link for actual answer
https://gist.github.com/int128/e47217bebdb4c402b2ffa7cc199307ba
Made some changes from above referred solution , request and response will log in console and in file too if logger level is info. we can print either in console or file.
#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);
}
}
}
Output in File:
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}
Here my solution (Spring 2.0.x)
Add the maven dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Edit the application.properties and add the following line:
management.endpoints.web.exposure.include=*
Once your spring boot application is started you can track the latest 100 http requests by calling this url:
http://localhost:8070/actuator/httptrace
You can also configure a custom Spring interceptor HandlerInterceptorAdapter for a simplified implementation of pre-only/post-only interceptors:
#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
}
}
Then, you register as many interceptors as you want:
#Configuration
public class WebMvcConfig implements WebMvcConfigurer {
#Autowired
CustomHttpInterceptor customHttpInterceptor;
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(customHttpInterceptor).addPathPatterns("/endpoints");
}
}
Note: just like stated by #Robert, you need to pay attention to the specific implementations of HttpServletRequest and HttpServletResponse your application is using.
For example, for apps using the ShallowEtagHeaderFilter, the response implementation would be a ContentCachingResponseWrapper, so you'd have:
#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";
}
}
}
the code pasted below works with my tests and can be downloaded from my [github project][1], sharing after applying a solution based on that on a production project.
#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;
}
}
}
If somebody still need it here is simple implementation with Spring HttpTrace Actuator. But as they have told upper it doesn't log bodies.
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()));
}
}
#hahn's answer required a bit of modification for it to work for me, but it is by far the most customizable thing I could get.
It didn't work for me, probably because I also have a HandlerInterceptorAdapter[??] but I kept getting a bad response from the server in that version. Here's my modification of it.
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);
}
}
Has there been any development with Actuator HTTP Trace since the initial question was posted i.e. is there a way to enrich it with the response body?
What about enriching it with custom metadata from MDC or from Spring-Sleuth or Zipkin, such as traceId and spanId?
Also for me Actuator HTTP Trace didn't work Spring Boot 2.2.3, and I found the fix here: 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
The fix:
The simple fix for this problem is, to add a #Bean of type
InMemoryHttpTraceRepository to your #Configuration-class:
#Bean
public HttpTraceRepository htttpTraceRepository()
{
return new InMemoryHttpTraceRepository();
}
The Explanation:
The cause of this problem is not a bug, but a legitimate change in the
default configuration. Unfortunately, this change is not noted in the
according section of the documentation. Instead it is burried in the
Upgrade Notes for Spring Boot 2.2
The default-implementation stores the captured data in memory. Hence,
it consumes much memory, without the user knowing, or even worse:
needing it. This is especially undesirable in cluster environments,
where memory is a precious good. And remember: Spring Boot was
invented to simplify cluster deployments!
That is, why this feature is now turned of by default and has to be
turned on by the user explicitly, if needed.
If you are seeing only part of your request payload, you need to call the setMaxPayloadLength function as it defaults to showing only 50 characters in your request body. Also, setting setIncludeHeaders to false is a good idea if you don't want to log your auth headers!
#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;
}
As suggested previously, Logbook is just about perfect for this, but I did have a little trouble setting it up when using Java modules, due to a split package between logbook-api and logbook-core.
For my Gradle + Spring Boot project, I needed
build.gradle
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>
In order to log all the requests with input parameters and body, we can use filters and interceptors. But while using a filter or interceptor, we cannot print the request body multiple times.
The better way is we can use spring-AOP. By using this we can decouple the logging mechanism from the application. AOP can be used for logging Input and output of each method in the application.
My solution is:
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();
}
}
Note
#Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
...
}
approach will not work with spring security filter chains.
You have to manually add CommonsRequestLoggingFilter like
protected void configure(HttpSecurity http) throws Exception {
HttpSecurity filter = http
.cors().and().addFilterBefore(new CommonsRequestLoggingFilter(), CorsFilter.class);
}
if you use Tomcat in your boot app here is org.apache.catalina.filters.RequestDumperFilter in a class path for you. (but it will not provide you "with exceptions in single place").
If you have Spring boot Config server configured then just enable Debug logger for class :
Http11InputBuffer.Http11InputBuffer.java
Debugs will log all the requests and responses for every request
I created a file called LoggingConfig.java with contents:
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;
}
}
In application.properties I added:
logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG
Logging Request + payload in a CUSTOM FORMAT:
For a custom format, just override the super implementation of the Spring logger Bean - org/springframework/web/filter/AbstractRequestLoggingFilter.java
Let's say we whish skipping GET requests and only tracing write requests (PUT, PATCH, DELETE, etc) with INFO log level:
#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;
}
Loggging Requeest + Response/status:
see https://www.baeldung.com/spring-http-logging#custom-request-logging
(I can add the exact code example to here if the answer gets the demand/ reaches 50+ upvotes)
You can use Actuator in Spring Boot.
It logs requests and responses and more information about servlet and system operating.
You just add it as a dependency of your project and config, if more details needed to log.
Take a look at this example:
Actuator in Spring Boot example
In order to log requests that result in 400 only:
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);
}
}
}
you can use Aspect Oriented Programming To Handle All These In One Plcae.

Categories

Resources