Slf4jMdcUtil.java

package se.jobtechdev.personaldatagateway.api.logging;

import org.slf4j.Logger;
import org.slf4j.MDC;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Iterator;

public final class Slf4jMdcUtil {

  public static final String EVENT_ACTION = "event.action";
  public static final String AUDIT_LOG = "audit.log";
  public static final String TEMP_LOG = "temp.log";
  public static final String TEMP_CUSTOM_PDG_API_CLIENTID = "temp.custom.pdg-api.clientid";
  public static final String AUDIT_CUSTOM_PDG_API_CLIENTID = "audit.custom.pdg-api.clientid";
  public static final String HTTP_REQUEST_METHOD = "http.request.method";
  public static final String HTTP_RESPONSE_MIME_TYPE = "http.response.mime_type";
  public static final String HTTP_RESPONSE_STATUS_CODE = "http.response.status_code";
  public static final String URL_PATH = "url.path";
  public static final String URL_QUERY = "url.query";
  public static final String URL_FULL = "url.full";
  public static final String URL_PORT = "url.port";
  public static final String URL_SCHEME = "url.scheme";
  public static final String OBJECT_ID = "object.id";
  public static final String EVENT_UPDATE_PARAMETERS = "event.update_parameters";
  public static final String EVENT_CREATE_PARAMETERS = "event.create_parameters";
  public static final String EVENT_SEARCH_HITS = "event.search.hits";
  public static final String EVENT_SEARCH_PARAMETERS = "event.search.parameters";
  public static final String EVENT_OUTCOME = "event.outcome";
  public static final String EVENT_ACTION_READ = "read";
  public static final String EVENT_ACTION_CREATE = "create";
  public static final String EVENT_ACTION_UPDATE = "update";
  public static final String EVENT_OUTCOME_SUCCESS = "success";
  public static final String EVENT_OUTCOME_FAILURE = "failure";
  public static final String OBJECT_BODY = "object.body";

  private Slf4jMdcUtil() {}

  public static boolean activateAuditField() {
    final var tempLog = MDC.get(TEMP_LOG);

    if (null != tempLog) {
      MDC.put(AUDIT_LOG, tempLog);
      final var clientId = MDC.get(TEMP_CUSTOM_PDG_API_CLIENTID);
      if (null != clientId) MDC.put(AUDIT_CUSTOM_PDG_API_CLIENTID, clientId);
      return true;
    }
    return false;
  }

  public static void addHttpFields(
      ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) {
    MDC.put(HTTP_REQUEST_METHOD, request.getMethod());
    MDC.put(HTTP_RESPONSE_MIME_TYPE, response.getContentType());
    MDC.put(HTTP_RESPONSE_STATUS_CODE, Integer.toString(response.getStatus()));
  }

  public static void addUrlFields(ContentCachingRequestWrapper request) {
    MDC.put(URL_PATH, UrlFieldExtractor.extractUrlPath(request));
    MDC.put(URL_QUERY, UrlFieldExtractor.extractUrlQuery(request));
    MDC.put(URL_FULL, UrlFieldExtractor.extractUrlFull(request));
    MDC.put(URL_PORT, UrlFieldExtractor.extractUrlPort(request));
    MDC.put(URL_SCHEME, UrlFieldExtractor.extractUrlScheme(request));
  }

  public static void addEventFields(
      ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) {
    final var paramStringBuilder = new StringBuilder();
    final var paramNames = request.getParameterNames();
    for (Iterator<String> it = paramNames.asIterator(); it.hasNext(); ) {
      var paramName = it.next();
      var paramValues = String.join(",", request.getParameterValues(paramName));
      paramStringBuilder.append(String.format("%s=%s", paramName, paramValues));
      if (it.hasNext()) {
        paramStringBuilder.append(";");
      }
    }

    switch (request.getMethod()) {
      case "GET":
        {
          MDC.put(EVENT_ACTION, EVENT_ACTION_READ);
          final var xPaginationTotal = response.getHeader("X-Pagination-Total");
          if (xPaginationTotal != null && !xPaginationTotal.isEmpty()) {
            MDC.put(EVENT_SEARCH_HITS, xPaginationTotal);
          }
          if (!paramStringBuilder.isEmpty()) {
            MDC.put(EVENT_SEARCH_PARAMETERS, paramStringBuilder.toString());
          }
          break;
        }
      case "POST":
        {
          MDC.put(EVENT_ACTION, EVENT_ACTION_CREATE);
          if (!paramStringBuilder.isEmpty()) {
            MDC.put(EVENT_CREATE_PARAMETERS, paramStringBuilder.toString());
          }

          break;
        }
      case "PATCH":
        {
          MDC.put(EVENT_ACTION, EVENT_ACTION_UPDATE);
          if (!paramStringBuilder.isEmpty()) {
            MDC.put(EVENT_UPDATE_PARAMETERS, paramStringBuilder.toString());
          }

          MDC.put(OBJECT_ID, request.getRequestURI());

          break;
        }
      default:
        {
          /* NOP */
        }
    }
  }

  public static void log(ContentCachingResponseWrapper response, Logger logger) {
    final var message = ApplicationFieldExtractor.extractMessage(response);
    if (response.getStatus() < 400) {
      MDC.put(EVENT_OUTCOME, EVENT_OUTCOME_SUCCESS);
      logger.info(message);
    } else {
      MDC.put(EVENT_OUTCOME, EVENT_OUTCOME_FAILURE);
      logger.error(message);
    }
  }

  public static void addObjectFields(ContentCachingRequestWrapper request, Logger logger) {
    switch (request.getMethod()) {
      case "POST":
        {
          Slf4jMdcUtil.addObjectBodyField(request, logger);
          break;
        }
      case "PATCH":
        {
          MDC.put(OBJECT_ID, request.getRequestURI());
          Slf4jMdcUtil.addObjectBodyField(request, logger);
          break;
        }
      default:
        {
          /* NOP */
        }
    }
  }

  public static void addObjectBodyField(ContentCachingRequestWrapper request, Logger logger) {
    final var encoded = request.getContentAsString();
    if (!encoded.isEmpty()) {
      try {
        final var decoded = URLDecoder.decode(encoded, request.getCharacterEncoding());
        if (!decoded.isEmpty()) {
          MDC.put(OBJECT_BODY, decoded);
        }
      } catch (UnsupportedEncodingException exception) {
        logger.warn(
            "Failed to decode request content, writing content as-is to 'object.body' field...");
        MDC.put(OBJECT_BODY, encoded);
      }
    }
  }

  public static void clearMdc() {
    MDC.clear();
  }
}