SOAP & Rest Logging Request/Response
import javax.xml.ws.handler.soap.SOAPHandler;
public abstract class AbstractLoggingHandler implements SOAPHandler
private static final String INCOMING = "SoapMessage Incoming :";
private static final String OUTGOING = "SoapMessage Outgoing :";
private static final String ENCODING = "UTF-8";
/**
* Gets the logger for the logging handler.
* @return the logger.
*/
public abstract Logger getLogger();
/**
* Gets the prefix of a section to replace (e.g. a string known to prefix a password field).
* @return the start of the section to replace.
*/
public abstract String getReplacementSectionPrefix();
/**
* Gets the postfix of a section to replace (e.g. a string known to follow a password field).
* @return the start of the section to replace.
*/
public abstract String getReplacementSectionPostfix();
/**
* Gets the value to determine if replacement should take place.
* Replacement will occur unless the value returned is equal to "false" ignoring case.
* A boolean value is NOT used here so that all logging handlers treat values other than
* "false" ignoring case in a uniform way.
* @return the value to determine if replacement should take place.
*/
public abstract String getReplacementDeterminant();
/**
* Creates the replacement value.
* @param valueToReplace the value to replace.
* @return the value to replace.
*/
public abstract String createReplacement(final String valueToReplace);
/**
* Logs the in-bound and out-bound messages.
* @param context the context.
* @return always returns true.
*/
@Override
public boolean handleMessage(final SOAPMessageContext context) {
return log(context);
}
/**
* Logs the fault messages.
* @param context the context.
* @return always returns true.
*/
@Override
public boolean handleFault(final SOAPMessageContext context) {
return log(context);
}
private boolean log(final SOAPMessageContext context) {
try {
final String xml = performReplacement(getXML(context));
final boolean outboundProperty = (Boolean) context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
getLogger().info((outboundProperty ? OUTGOING : INCOMING) + xml);
} catch (final Exception e) {
getLogger().error("Error logging from SOAPMessageContext", e);
}
return true;
}
String performReplacement(final String xml) {
final StringBuilder sb = new StringBuilder(xml == null ? "" : xml);
if (isReplacementViable(xml)) {
getLogger().debug("all replacement parameters viable");
final int startIndex = sb.indexOf(getReplacementSectionPrefix());
if (startIndex != -1) {
getLogger().debug("start of replacement found");
final int start = startIndex + getReplacementSectionPrefix().length();
final int end = sb.indexOf(getReplacementSectionPostfix(), start);
if (end != -1) {
getLogger().debug("finding value to replacement");
final String valueToReplace = sb.substring(start, end);
final String replacement = createReplacement(valueToReplace);
if (replacement != null) {
getLogger().debug("performing replacement");
sb.replace(start, end, replacement);
}
}
}
}
return sb.toString();
}
private boolean isReplacementViable(final String xml) {
return xml != null && !xml.isEmpty() &&
!Boolean.FALSE.toString().equalsIgnoreCase(getReplacementDeterminant()) &&
getReplacementSectionPrefix() != null && !getReplacementSectionPrefix().isEmpty() &&
getReplacementSectionPostfix() != null && !getReplacementSectionPostfix().isEmpty();
}
protected String getXML(final SOAPMessageContext soapMessageContext) throws Exception {
final SOAPMessage message = soapMessageContext.getMessage();
final ByteArrayOutputStream stream = new ByteArrayOutputStream();
message.writeTo(stream);
return stream.toString(ENCODING);
}
@Override
public void close(final MessageContext context) {
// No resources to close
}
@Override
public Set
return Collections.emptySet();
}
public Binding configureLoggingHandler(final Binding clientBinding, final AbstractLoggingHandler abstractLoggingHandler) {
@SuppressWarnings("rawtypes")
final List
handlerList.clear();
handlerList.add(abstractLoggingHandler);
clientBinding.setHandlerChain(handlerList);
return clientBinding;
}
}
Then each client cna extend this class which may be kept in library
----------------------
For Rest Based
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.annotation.Priority;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.client.ClientResponseContext;
import javax.ws.rs.client.ClientResponseFilter;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.WriterInterceptor;
import javax.ws.rs.ext.WriterInterceptorContext;
import org.apache.commons.lang.BooleanUtils;
import org.glassfish.jersey.media.multipart.BodyPart;
import org.glassfish.jersey.media.multipart.BodyPartEntity;
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
import org.glassfish.jersey.message.MessageUtils;
import org.glassfish.jersey.message.internal.MediaTypes;
import org.glassfish.jersey.server.ContainerRequest;
import org.glassfish.jersey.server.ContainerResponse;
import org.slf4j.Logger;
/**
* This is the Abstract Logging Filter Class for logging payload on RESTful interfaces using JERSEY library (> version 2.22)
*
* /
@PreMatching
@Priority(Integer.MIN_VALUE)
@SuppressWarnings("ClassWithMultipleLoggers")
public abstract class AbstractLoggingFilter
implements ContainerRequestFilter, ClientRequestFilter, ContainerResponseFilter, ClientResponseFilter, WriterInterceptor {
private static final int CONSTANT_1024 = 1024;
private static final String NOTIFICATION_PREFIX = "\n ";
private static final String REQUEST_PREFIX = " ";
private static final String RESPONSE_PREFIX = " ";
private static final String ENTITY_LOGGER_PROPERTY = AbstractLoggingFilter.class.getName() + ".entityLogger";
private static final Comparator
@Override
public int compare(final Map.Entry
return o1.getKey().compareToIgnoreCase(o2.getKey());
}
};
private static final String INCOMING_LOG_PREFIX = "RESTMessage Incoming: ";
private static final String OUTGOING_LOG_PREFIX = "RESTMessage Outgoing: ";
private static final String HEADER_SEPARATOR = "=";
private static final String OUTGOING_PAYLOAD_PREFIX = "OutgoingBody=";
private static final String INCOMING_PAYLOAD_PREFIX = "IncomingBody=";
private static final String INCOMING_HEADERS_PREFIX = "IncomingHeaders=";
private static final String OUTGOING_HEADERS_PREFIX = "OutgoingHeaders=";
//
@SuppressWarnings("NonConstantLogger")
protected final Logger logger;
/**
* Create a logging filter with custom logger and custom settings of entity logging.
* @param logger the logger to log requests and responses.
*
*/
@SuppressWarnings("BooleanParameter")
public AbstractLoggingFilter(final Logger logger) {
this.logger = logger;
}
/**
* @param b, StringBuilder
*/
private void log(final StringBuilder b) {
if (logger != null) {
logger.info(b.toString());
}
}
private String removeCarriageReturns(String entity) {
return entity != null ? entity.replaceAll("[\r\n]", "") : entity;
}
/**
* @param b, StringBuilder
* @param note, String note
* @param method, HTTP-method
* @param uri, URI instance
*/
private void printRequestLine(final StringBuilder b, final String note, final String method, final URI uri) {
b.append(NOTIFICATION_PREFIX).append(note);
b.append(REQUEST_PREFIX).append("HttpMethod=").append(method).append(" ").append("URL=").append(uri.toASCIIString())
.append(" ");
}
/**
* @param b, StringBuilder
* @param note, note
* @param status, HTTP State Code
*/
private void printResponseLine(final StringBuilder b, final String note, final int status) {
b.append(NOTIFICATION_PREFIX).append(note);
b.append(RESPONSE_PREFIX).append("ResponseStatusCode=").append(Integer.toString(status)).append(" ");
}
/**
* @param b, StringBuilder
* @param prefix, String
* @param headers, MultivaluedMap
*/
private void printPrefixedHeaders(final StringBuilder b, final String prefix, final MultivaluedMap
boolean isReplacementNeeded = BooleanUtils.toBoolean(getReplacementDeterminant());
StringBuilder headersSB = new StringBuilder();
for (final Map.Entry
final List val = headerEntry.getValue();
final String header = headerEntry.getKey();
String valueToPrint = null;
if (val.size() == 1) {
valueToPrint = val.get(0) != null ? val.get(0).toString() : null;
} else {
final StringBuilder sb = new StringBuilder();
boolean add = false;
for (final Object s : val) {
if (add) {
sb.append(',');
}
add = true;
sb.append(s);
}
valueToPrint = sb.toString();
}
// Mask the header if configured to mask.
if (isReplacementNeeded && getHTTPHeadersToMask() != null
&& getHTTPHeadersToMask().contains(header) && valueToPrint != null) {
valueToPrint = createReplacement(valueToPrint);
}
headersSB.append(prefix).append(header.trim()).append(HEADER_SEPARATOR).append(valueToPrint).append(", ");
}
// remove extra comma at the end.
int lastCommaIndex = headersSB.lastIndexOf(",");
if (lastCommaIndex != -1) {
headersSB.replace(lastCommaIndex, lastCommaIndex + 1, "");
}
b.append(headersSB);
}
/**
* @param headers, HTTP Headers as Set
* @return sorted set.
*/
private Set
final SortedSet
sortedHeaders.addAll(headers);
return sortedHeaders;
}
/**
* @param stream, InputString
* @param charset, Charset
* @return Entity as String
* @throws IOException
*/
private String getEntityFromInputStream(InputStream stream, final Charset charset) throws IOException {
BufferedInputStream inputStream = new BufferedInputStream(stream);
StringBuilder b = new StringBuilder();
byte[] contents = new byte[CONSTANT_1024];
int bytesRead;
while ((bytesRead = inputStream.read(contents)) != -1) {
b.append(new String(contents, 0, bytesRead, charset));
}
return b.toString();
}
/**
* Get the JSON OR XML from a multipart form in a server request.
* @param multiPart
* @param charset
* @return The XML OR JSON. May be empty if no XML / JSON part found.
* @throws IOException
*/
private String getMultipartJsonXmlStringFromRequest(final ContainerRequestContext context, final Charset charset)
throws IOException {
String multipartEntity = "";
FormDataMultiPart multiPart = ((ContainerRequest) context).readEntity(FormDataMultiPart.class);
if (multiPart != null) {
for (BodyPart part : multiPart.getBodyParts()) {
logger.debug("multi form parts found. Check to see if it is json / xml");
if (isMediaTypeAbleToLog(part.getMediaType())) {
logger.debug("multi form XML / JSON part found.");
multipartEntity = getEntityFromInputStream(((BodyPartEntity) part.getEntity()).getInputStream(), charset);
}
}
}
return multipartEntity;
}
/**
* Get the JSON OR XML from a multipart from in a server response.
* @param responseContext
* @return The XML OR JSON. May be empty if no XML / JSON part found.
* @throws IOException
*/
private String getMultipartJsonXmlStringFromResponse(final ContainerResponseContext responseContext) throws IOException {
String multipartEntity = "";
FormDataMultiPart multiPart = (FormDataMultiPart) ((ContainerResponse) responseContext).getEntity();
if (multiPart != null) {
for (BodyPart part : multiPart.getBodyParts()) {
logger.debug("multi form parts found. Check to see if it is json / xml");
if (isMediaTypeAbleToLog(part.getMediaType())) {
logger.debug("multi form XML / JSON part found.");
multipartEntity = (String) part.getEntity();
}
}
}
return multipartEntity;
}
/**
* Check to see if the given media type is allowed to be printed in the log file.
* Binary types for example should not be printed as they take up pointless log space.
* @param type
* @return true if the media type can be logged.
*/
private boolean isMediaTypeAbleToLog(MediaType type) {
if (MediaTypes.typeEqual(type, MediaType.APPLICATION_JSON_TYPE) || MediaTypes.typeEqual(type, MediaType.TEXT_XML_TYPE)
|| MediaTypes.typeEqual(type, MediaType.APPLICATION_XML_TYPE)
|| MediaTypes.typeEqual(type, MediaType.TEXT_PLAIN_TYPE)) {
return true;
}
return false;
}
/**
* Log the output for the outgoing request from a CLIENT.
*/
@Override
public void filter(final ClientRequestContext context) throws IOException {
final StringBuilder b = new StringBuilder();
printRequestLine(b, OUTGOING_LOG_PREFIX, context.getMethod(), context.getUri());
b.append(OUTGOING_HEADERS_PREFIX);
printPrefixedHeaders(b, REQUEST_PREFIX, context.getStringHeaders());
b.append(OUTGOING_PAYLOAD_PREFIX);
if (context.hasEntity()) {
final LoggingStream stream = new LoggingStream(b, context.getEntityStream());
stream.setCanLogEntity(true);
context.setEntityStream(stream);
context.setProperty(ENTITY_LOGGER_PROPERTY, stream);
// not calling log(b) here - it will be called by the interceptor
} else {
log(b);
}
}
/**
* Log the incoming response to the CLIENT.
*/
@Override
public void filter(final ClientRequestContext requestContext, final ClientResponseContext responseContext)
throws IOException {
final StringBuilder b = new StringBuilder();
printResponseLine(b, INCOMING_LOG_PREFIX, responseContext.getStatus());
b.append(INCOMING_HEADERS_PREFIX);
printPrefixedHeaders(b, RESPONSE_PREFIX, responseContext.getHeaders());
b.append(INCOMING_PAYLOAD_PREFIX);
if (responseContext.hasEntity()) {
Charset charset = MessageUtils.getCharset(responseContext.getMediaType());
String entity = getEntityFromInputStream(responseContext.getEntityStream(), charset);
b.append(removeCarriageReturns(performReplacement(entity, true)));
BufferedInputStream inputStream = new BufferedInputStream(new ByteArrayInputStream(entity.getBytes()));
responseContext.setEntityStream(inputStream);
}
log(b);
}
/**
* Log the input from the incoming request to the server.
*/
@Override
public void filter(final ContainerRequestContext context) throws IOException {
final StringBuilder b = new StringBuilder();
printRequestLine(b, INCOMING_LOG_PREFIX, context.getMethod(), context.getUriInfo().getRequestUri());
b.append(INCOMING_HEADERS_PREFIX);
printPrefixedHeaders(b, REQUEST_PREFIX, context.getHeaders());
b.append(INCOMING_PAYLOAD_PREFIX);
String entity;
if (context.hasEntity()) {
((ContainerRequest) context).bufferEntity();
Charset charset = MessageUtils.getCharset(context.getMediaType());
if (context.getMediaType() != null
&& MediaTypes.typeEqual(context.getMediaType(), MediaType.MULTIPART_FORM_DATA_TYPE)) {
logger.debug("Multipart from request.");
entity = getMultipartJsonXmlStringFromRequest(context, charset);
} else {
entity = getEntityFromInputStream(context.getEntityStream(), charset);
}
b.append(removeCarriageReturns(performReplacement(entity, true)));
}
log(b);
}
/**
* Log the output for the outgoing response from the SERVER.
*/
@Override
public void filter(final ContainerRequestContext requestContext, final ContainerResponseContext responseContext)
throws IOException {
final StringBuilder b = new StringBuilder();
printResponseLine(b, OUTGOING_LOG_PREFIX, responseContext.getStatus());
b.append(OUTGOING_HEADERS_PREFIX);
printPrefixedHeaders(b, RESPONSE_PREFIX, responseContext.getStringHeaders());
b.append(OUTGOING_PAYLOAD_PREFIX);
if (responseContext.hasEntity()) {
LoggingStream stream = new LoggingStream(b, responseContext.getEntityStream());
if (responseContext.getMediaType() != null
&& MediaTypes.typeEqual(responseContext.getMediaType(), MediaType.MULTIPART_FORM_DATA_TYPE)) {
logger.debug("Multipart form response");
String entity = getMultipartJsonXmlStringFromResponse(responseContext);
stream.setMultipartBody(entity);
stream.setCanLogEntity(true);
} else {
stream.setCanLogEntity(isMediaTypeAbleToLog(responseContext.getMediaType()));
}
requestContext.setProperty(ENTITY_LOGGER_PROPERTY, stream);
responseContext.setEntityStream(stream);
// not calling log(b) here - it will be called by the interceptor
} else {
log(b);
}
}
/**
* This is where the log message is actually written on the server response.
* The write interceptor is fired after the container response so writing it here means no further changes have been made.
*/
@Override
public void aroundWriteTo(final WriterInterceptorContext writerInterceptorContext) throws IOException {
final LoggingStream stream = (LoggingStream) writerInterceptorContext.getProperty(ENTITY_LOGGER_PROPERTY);
writerInterceptorContext.proceed();
if (stream != null) {
String entity = stream.getEntity(MessageUtils.getCharset(writerInterceptorContext.getMediaType()));
if (entity != null && entity != "") {
entity = removeCarriageReturns(performReplacement(entity, false));
stream.getStringBuilder().append(entity);
}
log(stream.getStringBuilder());
}
}
/**
* Performs any necessary replacement.
* @param entity, the entity before replacement.
* @param isIncoming, indicates whether replacement is to be done for incoming request or outgoing response.
* @return the entity after replacement.
*/
protected abstract String performReplacement(final String entity, boolean isIncoming);
/**
* Gets the value to determine if replacement should take place. Replacement will occur unless the value returned is equal to
* "false" ignoring case. A boolean value is NOT used here so that all logging handlers treat values other than "false"
* ignoring case in a uniform way.
* @return the value to determine if replacement should take place.
*/
public abstract String getReplacementDeterminant();
/**
* Creates the replacement value.
* @param valueToReplace the value to replace.
* @return the value to replace.
*/
public abstract String createReplacement(final String valueToReplace);
/**
* The list of headers whose values are to be masked before logging.
*
* If no masking is required, the implementing method can return null.
*
* @return List of String.
*/
public abstract List
}
-------
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
/**
* Abstract JSON Logging Filter class.
* This class should be extended and implemented to log JSON payloads.
* It provides facilities to mask the JSON values based on the keys configured.
*
*/
public abstract class AbstractJSONLoggingFilter extends AbstractLoggingFilter{
private static final int INDENT_BY_2_SPACES = 2;
/**
* Constructor.
* @param logger
*
*/
public AbstractJSONLoggingFilter(Logger logger) {
super(logger);
}
/**
* Should return null or a valid JSON key path which points to the value to be masked in the Incoming JSON Object graph.
* If null, then no masking happens.
*
* For.e.g - 'Request.requestHeader.security' key path can be used to mask the value of 'security' JSON field in the following
* JSON String.
*
*{
* Request: {
* requestHeader: {
* security: password
* }
* }
* }
*
*
* @return JSON Key Path to be masked.
*/
protected abstract String getIncomingKeyPathForMasking();
/**
* Should return null or a valid JSON key path which points to the value to be masked in the Outgoing JSON Object graph.
* If null, then no masking happens.
*
* For.e.g - 'Response.responseHeader.security' key path can be used to mask the value of 'security' JSON field in the following
* JSON String.
*
*{
* Response: {
* responseHeader: {
* security: password
* }
* }
* }
*
*
* @return JSON Key Path to be masked.
*/
protected abstract String getOutgoingKeyPathForMasking();
@Override
protected String performReplacement(String entity, boolean isIncoming) {
String maskedEntity = entity;
if (isReplacementViable(entity, isIncoming)) {
String jsonKeyPath = isIncoming ? getIncomingKeyPathForMasking() : getOutgoingKeyPathForMasking();
logger.info("Replacing JSON Key Path - {}", jsonKeyPath);
try {
JSONObject jsonObject = new JSONObject(entity);
jsonObject = traversePathAndReplace(jsonKeyPath, jsonObject);
maskedEntity = jsonObject.toString(INDENT_BY_2_SPACES);
} catch (Exception e) {
logger.info("Unable to parse the JSON Text and perform masking for '{}' - {}", jsonKeyPath, e.getMessage());
}
}
return maskedEntity;
}
/**
* This method recursively traverses the JSON object graph based on the path provided.
* if the path is found it calls createReplacement on the value at the path and updated the JSONObject.
*
* @param path
* @param jsonObject
* @return JSONObject with masked values.
* @throws JSONException
*/
private JSONObject traversePathAndReplace(String path, JSONObject jsonObject) throws JSONException {
logger.debug("Path is {}", path);
if (path.indexOf(".") == -1) {
String value = (String) jsonObject.get(path);
value = createReplacement(value);
jsonObject.put(path,value);
} else {
String currentPath = path.substring(0, path.indexOf("."));
logger.debug("Current Path is {}",currentPath);
String nextPath = path.substring(path.indexOf(".")+1, path.length());
logger.debug("Next Path is {}",nextPath);
JSONObject updateJsonObj = traversePathAndReplace(nextPath, jsonObject.getJSONObject(currentPath));
jsonObject.put(currentPath, updateJsonObj);
}
return jsonObject;
}
/**
* Determines if replacement can proceed by checking all the necessary preconditions.
*
* @param entity the Entity before replacement.
* @param isIncoming @return true if all the necessary preconditions exist otherwise false.
*/
private boolean isReplacementViable(final String entity, boolean isIncoming) {
return !StringUtils.isBlank(entity.toString()) &&
(!StringUtils.isBlank(getIncomingKeyPathForMasking()) && isIncoming ||
!StringUtils.isBlank(getOutgoingKeyPathForMasking()) && !isIncoming) &&
BooleanUtils.toBoolean(getReplacementDeterminant());
}
}
No comments:
Post a Comment