package org.greeneyed.summer.util.logging; /* * #%L * Summer * %% * Copyright (C) 2018 GreenEyed (Daniel Lopez) * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 2.1 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Lesser Public License for more details. * * You should have received a copy of the GNU General Lesser Public * License along with this program. If not, see * <http://www.gnu.org/licenses/lgpl-2.1.html>. * #L% */ import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.charset.UnsupportedCharsetException; import java.util.List; import org.greeneyed.summer.util.ObjectJoiner; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpRequest; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import org.springframework.util.StreamUtils; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; /** * Allows logging outgoing requests and the corresponding responses. Requires * the use of a {@link org.springframework.http.client.BufferingClientHttpRequestFactory} to * log the body of received responses. */ @Slf4j public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { public static final String CONTENT_TYPE_HEADER = "Content-Type"; public static final String CONTENT_LENGTH_HEADER = "Content-Length"; public static final String CONTENT_ENCODING_HEADER = "Content-Encoding"; public static final String APPLICATION_JSON_HEADER = "application/json"; private volatile boolean loggedMissingBuffering; @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { if (log.isInfoEnabled()) { logRequest(request, body); } ClientHttpResponse response = execution.execute(request, body); if (log.isInfoEnabled()) { logResponse(request, response); } return response; } protected void logRequest(HttpRequest request, byte[] body) { log.info("Request: {}", ObjectJoiner.join(" ", request.getURI().getScheme(), request.getMethod(), request.getURI())); final boolean hasRequestBody = body != null && body.length > 0; if (log.isDebugEnabled()) { // If the request has a body, sometimes these headers are not // present, so let's make them explicit if (hasRequestBody) { logHeader(CONTENT_LENGTH_HEADER, Long.toString(body.length)); final MediaType contentType = request.getHeaders().getContentType(); if (contentType != null) { logHeader(CONTENT_TYPE_HEADER, contentType.toString()); } } // Log the other headers for (String header : request.getHeaders().keySet()) { if (!CONTENT_TYPE_HEADER.equalsIgnoreCase(header) && !CONTENT_LENGTH_HEADER.equalsIgnoreCase(header)) { for (String value : request.getHeaders().get(header)) { logHeader(header, value); } } } if (log.isTraceEnabled() && hasRequestBody) { logBody(new String(body, determineCharset(request.getHeaders())), request.getHeaders()); } } } protected void logResponse(HttpRequest request, ClientHttpResponse response) { try { log.info("Response: {}", ObjectJoiner.join(" ", response.getRawStatusCode(), response.getStatusText(), " from ", request.getMethod(), ": ", request.getURI())); if (log.isDebugEnabled()) { HttpHeaders responseHeaders = response.getHeaders(); for (String header : response.getHeaders().keySet()) { for (String value : response.getHeaders().get(header)) { logHeader(header, value); } } if (log.isTraceEnabled() && hasTextBody(responseHeaders) && isBuffered(response)) { logBody(StreamUtils.copyToString(response.getBody(), determineCharset(responseHeaders)), responseHeaders); } } } catch (IOException e) { log.warn("Failed to log response for {} request to {}", request.getMethod(), request.getURI(), e); } } private void logHeader(final String headerName, final String headerValue) { log.debug(" Header: {}: \"{}\"", headerName, headerValue); } private void logBody(String body, HttpHeaders headers) { MediaType contentType = headers.getContentType(); List<String> contentEncoding = headers.get(CONTENT_ENCODING_HEADER); if (contentEncoding != null && !contentEncoding.contains("identity")) { log.trace(" Body: encoded, not shown"); } else { if (contentType != null && contentType.toString().startsWith(APPLICATION_JSON_HEADER)) { log.trace(" Body: {}", writeJSON(body)); } else { log.trace(" Body: {}", body); } } } private static String writeJSON(final Object object) { ObjectMapper mapper = null; String result = null; mapper = new ObjectMapper(); try { if (object instanceof String) { Object json = mapper.readValue((String) object, Object.class); result = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(json); } else { result = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(object); } } catch (final IOException e) { log.warn("Body is not a json object {}", e.getMessage()); } return result; } private Charset determineCharset(HttpHeaders headers) { Charset resultCharset = StandardCharsets.UTF_8; MediaType contentType = headers.getContentType(); if (contentType != null) { try { Charset charSet = contentType.getCharset(); if (charSet != null) { resultCharset = charSet; } } catch (UnsupportedCharsetException e) { log.error("Error setting charset", e); } } return resultCharset; } private boolean hasTextBody(HttpHeaders headers) { long contentLength = headers.getContentLength(); if (contentLength != 0) { MediaType contentType = headers.getContentType(); if (contentType != null) { String subtype = contentType.getSubtype(); return "text".equals(contentType.getType()) || "xml".equals(subtype) || "json".equals(subtype); } } return false; } private boolean isBuffered(ClientHttpResponse response) { // class is non-public, so we check by name boolean buffered = "org.springframework.http.client.BufferingClientHttpResponseWrapper".equals(response.getClass().getName()); if (!buffered && !loggedMissingBuffering) { log.warn("Can't log HTTP response bodies, as you haven't configured the RestTemplate with a BufferingClientHttpRequestFactory"); loggedMissingBuffering = true; } return buffered; } }