package play.modules.logger; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import play.Play; import play.PlayPlugin; import play.mvc.Http; import play.mvc.Scope; import play.mvc.results.*; import play.mvc.results.Error; import play.rebel.RenderView; import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; import static java.lang.System.currentTimeMillis; import static java.util.Arrays.asList; import static java.util.stream.Collectors.joining; public class RequestLogPlugin extends PlayPlugin { private static final String REQUEST_ID_PREFIX = Integer.toHexString((int)(Math.random() * 0x1000)); private static final AtomicInteger counter = new AtomicInteger(1); private static final Logger logger = LoggerFactory.getLogger("request"); private static final String LOG_AS_PATH = Play.configuration.getProperty("request.log.pathForAction", "Web."); @Override public void onConfigurationRead() { initMaskedParams(); } @Override public void routeRequest(Http.Request request) { request.args.put("startTime", currentTimeMillis()); request.args.put("requestId", REQUEST_ID_PREFIX + "-" + counter.incrementAndGet()); } @Override public void beforeActionInvocation(Method actionMethod) { Thread.currentThread().setName(getOriginalThreadName() + " " + Http.Request.current().action); } @Override public void afterActionInvocation() { Thread.currentThread().setName(getOriginalThreadName()); } private String getOriginalThreadName() { String name = Thread.currentThread().getName(); int i = name.indexOf(' '); return i == -1 ? name : name.substring(0, i); } @Override public void onActionInvocationFinally() { Http.Request request = Http.Request.current(); Result result = (Result) request.args.get("play.modules.logger.Result"); if (isAwait(request, result)) return; logRequestInfo(request, Scope.Session.current(), result); } @Override public void onActionInvocationResult(Result result) { Http.Request.current().args.put("play.modules.logger.Result", result); } public static void logRequestInfo(Http.Request request, Scope.Session session, Result result) { String executionTime = ""; if (request != null && request.args != null) { Long start = (Long) request.args.get("startTime"); if (start != null) executionTime = " " + (currentTimeMillis() - start) + " ms"; } logger.info(path(request) + ' ' + request.remoteAddress + ' ' + session.getId() + getRequestLogCustomData(request) + ' ' + extractParams(request) + " -> " + result(result) + executionTime); } private static boolean isAwait(Http.Request request, Result result) { return result == null && request.args.containsKey("__continuation"); } static String result(Result result) { return result == null ? "RenderError" : result instanceof Redirect ? toString((Redirect) result) : result instanceof RenderTemplate ? toString((RenderTemplate) result) : result instanceof RenderView ? toString((RenderView) result) : result instanceof RenderBinary ? toString((RenderBinary) result) : result instanceof Error ? toString((Error) result) : result.getClass().getSimpleName(); } private static String toString(Redirect result) { return result.getClass().getSimpleName() + ' ' + result.url; } private static String toString(RenderTemplate result) { return "RenderTemplate " + result.getName() + " " + result.getRenderTime() + " ms"; } private static String toString(RenderView result) { return "RenderView " + result.getName() + " " + result.getRenderTime() + " ms"; } private static String toString(RenderBinary result) { return Stream.of(result.getClass().getSimpleName(), result.getName(), result.getContentType()) .filter(StringUtils::isNotEmpty) .collect(joining(" ")); } private static String toString(Error result) { return result.getClass().getSimpleName() + " \"" + result.getMessage() + "\""; } static String getRequestLogCustomData(Http.Request request) { return request.args.containsKey("requestLogCustomData") ? " " + request.args.get("requestLogCustomData") : ""; } private static String path(Http.Request request) { return request.action == null || request.action.startsWith(LOG_AS_PATH) ? request.path : request.action; } private static final Set<String> SKIPPED_PARAMS = new HashSet<>(asList("authenticityToken", "action", "controller", "x-http-method-override", "body", "action", "controller")); private static final Set<String> MASKED_PARAMS = new HashSet<>(); private static void initMaskedParams() { String maskedParamsString = Play.configuration.getProperty("request.log.maskParams", "password|cvv|cardNumber|card.cvv|card.number"); for (String param : maskedParamsString.split("\\|")) MASKED_PARAMS.add(param.toLowerCase().trim()); } public static String extractParams(Http.Request request) { try { return extractParamsUnsafe(request); } catch (Exception e) { logger.error("Failed to parse request params", e); return ""; } } private static String extractParamsUnsafe(Http.Request request) { StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String[]> param : request.params.all().entrySet()) { String name = param.getKey(); if (SKIPPED_PARAMS.contains(name)) continue; sb.append('\t').append(name).append('='); String value; if (mustMask(name)) value = "*"; else { if (param.getValue().length == 1) value = param.getValue()[0]; else value = Arrays.toString(param.getValue()); } sb.append(value); } return sb.toString().trim(); } private static boolean mustMask(String name) { for (String maskedParam : MASKED_PARAMS) { if (name.toLowerCase().contains(maskedParam)) return true; } return false; } }