OmniFaces FullAjaxExceptionHandler 日志记录专业化

OmniFaces FullAjaxExceptionHandler logging specialization

我刚刚扩展了 OminFaces 的 FullAjaxExceptionHandler class 以添加我的项目所需的简单功能。

我所要做的就是简单地用我的记录器记录所有异常,并在日志和请求中添加一个唯一的错误标识符(具有特定的属性名称),该标识符将显示在错误页面中与其他信息。

这将允许快速检索日志中的错误(用户只需将此 "identifier" 发送给支持团队)。 我重写了 logException() 方法(尝试了两个重载),但它们从未被调用过。在 GitHub 上查看 FullAjaxExceptionHandler 的源代码,我看到它在某些特定情况下被调用。 所以我改写了 findErrorPageLocation() 方法(这是初步测试代码):

    public final static String ERROR_UUID = "javax.servlet.error.uuid";

    @Override
    protected String findErrorPageLocation(FacesContext context, Throwable exception) {            
        // Retrieve error page location from base class
        String location = super.findErrorPageLocation(context, exception); 
        // generate a unique error identifier
        String uuid = UUID.randomUUID().toString();
        // log into my logger
        logException(context, exception, location, "["+uuid+"]: "+exception.getMessage());
        // Retrieve request, put the new attribute to be shown into page and return the location
        HttpServletRequest req = (HttpServletRequest)context.getExternalContext().getRequest();
        req.setAttribute(ERROR_UUID, uuid);
        return location;
    }

在我刚刚添加的页面中

<li>Error UUID: #{requestScope['javax.servlet.error.uuid']}</li>

显示错误uuid。

好吧,这似乎行得通...但我不知道这里是否适合做此类事情。我宁愿重载 only logException() 方法,因为我想要专攻的是日志记录而不是其他行为。我做得好还是有更好的解决方案?

我正在使用 vanilla Java EE 7(所以是 JSF 2.2)、OmniFaces 2.6.9 和 Payara 5.181 以及 Java SE 1.8.0u170。

很抱歉没有回答你的直接问题,但也许我可以给你一个更好的整体解决方案,我们在生产中使用它,让你避免编辑 OmniFaces 代码,并使用 servlet 过滤器为你提供更强大的日志记录。

使用 servlet 过滤器,您可以利用 Log4J 或 SLF4J 映射设备上下文,将变量放在其使用的线程上。就像您一样,我们也使用 UUID 为每次 HTTP 请求生成一个唯一的 ID。

过滤器:

import java.io.IOException;
import java.util.UUID;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.HttpHeaders;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;


/**
 * Servlet filter used to attach incoming HTTP request values into the MDC so
 * they can be used throughout the entire request-&gt;response cycle for
 * logging.
 */
public class MDCLogFilter implements Filter {

   private static final Logger LOG = LoggerFactory.getLogger(MDCLogFilter.class);

   @Override
   public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
            throws IOException, ServletException {

      // add remote ip of incoming request
      MDC.put("ipv4", request.getRemoteAddr());

      // check for any specific Servlet values
      if (request instanceof HttpServletRequest) {
         final HttpServletRequest httpRequest = (HttpServletRequest) request;
         // try and get the username
         String username = httpRequest.getRemoteUser();

         // if not found then use their IP address as their username
         username = StringUtils.defaultIfBlank(username, request.getRemoteAddr());

         // uppercase the username for consistency
         MDC.put("user", StringUtils.upperCase(username));

         // first look for one passed in from the caller on the header
         String correlationId = httpRequest.getHeader("uuid");
         correlationId = StringUtils.defaultIfBlank(correlationId, UUID.randomUUID().toString());

         // unique ID for this thread context
         MDC.put("uuid", correlationId);
         request.setAttribute("javax.servlet.error.uuid", uuid);

         // get the browser user agent
         MDC.put("user-agent", httpRequest.getHeader(HttpHeaders.USER_AGENT));
      }

      try {
         chain.doFilter(request, response);
      } finally {
         MDC.clear();
      }
   }


}

在您的 web.xml 中,使用 *.

对每个传入的 HTTP 请求应用此过滤器

web.xml

<!-- Inject additional logging attributes into each servlet request -->
<filter>
    <filter-name>MDCFilter</filter-name>
    <filter-class>com.melloware.servlet.MDCLogFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>MDCFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Logback 和 Log4J 然后可以在日志输出中使用您的 MDC 值,因此它附加到每个日志行。我使用 SLF4J 和 Logback,所以我的示例在下面使用 %X 表示法在每个日志行中显示唯一 ID 和用户...

日志设置:

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender" >
    <file>application.log</file>
    <encoder>
        <pattern>%-5level - [id=%X{uuid} user=%X{user}] %logger{35} - %msg%n</pattern>
    </encoder>
</appender>

它有两个优点:

  1. 您可以在错误页面中使用 "javax.servlet.error.uuid",因为它已被填充到请求或响应中。
  2. 您日志中的所有日志消息都有 UUID,因此您可以跟踪整个请求,从客户通过您的所有逻辑提交到响应(或错误),快速轻松。如果客户在他们的错误页面上使用该 UUID 调用,您可以 GREP 您的日志并查看他们的整个事件链,包括堆栈跟踪。

FullAjaxExceptionHandler 不会记录 -Ajax JSF 请求的异常。非 Ajax JSF 请求(和非 JSF 请求!)的异常通常会到达 servlet Filter

findErrorPageLocation() 方法确实在 FullAjaxExceptionHandler 检查它是否是 Ajax 请求之前调用,因此您还可以记录非 Ajax JSF 请求的异常那里。

但是如果您想要记录非 JSF 请求的异常,那么您仍然需要一个 servlet Filter。在这种情况下,非 Ajax JSF 请求的异常最终将被记录两次。第一次通过自定义异常处理程序的 findErrorPageLocation() 方法,第二次通过 servlet Filter.

理想情况下,当您想为每个可能抛出的异常微调日志记录时,您最好同时拥有 自定义 FullAjaxExceptionHandler 和自定义 FacesExceptionHandler通过网络应用程序。

因此,一个用于 JSF Ajax 请求期间的异常:

public class CustomExceptionHandler extends FullAjaxExceptionHandler {

    public CustomExceptionHandler(ExceptionHandler wrapped) {
        super(wrapped);
    }

    @Override
    protected void logException(FacesContext context, Throwable exception, String location, LogReason reason) {
        String uuid = UUID.randomUUID().toString();
        String ip = FacesLocal.getRemoteAddr(context);
        FacesLocal.setRequestAttribute(context, "UUID", uuid);
        super.logException(context, exception, location, "[%s][%s] %s", uuid, ip, reason.getMessage(), location);
    }

    public static class Factory extends FullAjaxExceptionHandlerFactory {
        public Factory(ExceptionHandlerFactory wrapped) {
            super(wrapped);
        }

        @Override
        public ExceptionHandler getExceptionHandler() {
            return new CustomExceptionHandler(getWrapped().getExceptionHandler());
        }
    }
}

faces-config.xml中映射为

<factory>
    <exception-handler-factory>com.example.CustomExceptionHandler$Factory</exception-handler-factory>
</factory>

还有一个用于所有其他请求期间的异常:

@WebFilter(filterName="customExceptionFilter")
public class CustomExceptionFilter extends FacesExceptionFilter {

    private static final Logger logger = Logger.getLogger(CustomExceptionHandler.class.getName());

    @Override
    public void doFilter(HttpServletRequest request, HttpServletResponse response, HttpSession session, FilterChain chain) throws ServletException, IOException {
        try {
            super.doFilter(request, response, session, chain);
        }
        catch (Exception e) {
            String uuid = UUID.randomUUID().toString();
            String ip = Servlets.getRemoteAddr(request);
            request.setAttribute("UUID", uuid);
            logger.log(Level.SEVERE, String.format("[%s][%s]", uuid, ip), e);
            throw e;
        }
    }
}

web.xml 中映射为第一个 <filter-mapping> 条目:

<filter-mapping>
    <filter-name>customExceptionFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

让 OmniFaces 拥有 FullAjaxExceptionHandlerFacesExceptionFilter 来记录所有异常以及 UUID(和 IP)肯定是一个有用的改进。这确实不是一个不常见的业务需求。它将在下一个版本中出现。