【问题标题】:How to print server responses using LoggingFeature in Dropwizard 1.0.2?如何使用 Dropwizard 1.0.2 中的 LoggingFeature 打印服务器响应?
【发布时间】:2017-03-03 10:45:00
【问题描述】:

以下代码导致在 Dropwizard 0.9.2 和 1.0.2 中打印 JSON 服务器响应:

return ClientBuilder
        .newBuilder()
        .build()
        .register(new LoggingFilter(Logger.getLogger(LoggingFilter.class.getName()), true))

例如:

Oct 21, 2016 7:57:42 AM org.glassfish.jersey.filter.LoggingFilter log
INFO: 1 * Client response received on thread main
1 < 401
1 < Connection: keep-alive
1 < Content-Length: 49
1 < Content-Type: text/plain
1 < Date: Fri, 21 Oct 2016 07:57:42 GMT
1 < Server: […]
1 < WWW-Authenticate: Basic realm="[…]"
Credentials are required to access this resource.

javax.ws.rs.NotAuthorizedException: HTTP 401 Unauthorized

但是,LoggingFilter 在 1.0.2 中已弃用,建议改用 LoggingFeature。在documentation of LoggingFeature 中,它表示默认详细程度是LoggingFeature.Verbosity.PAYLOAD_TEXT,所以我期待以下代码仍能在 Dropwizard 1.0.2 中打印 JSON 服务器响应:

return ClientBuilder
        .newBuilder()
        .build()
        .register(new LoggingFeature(Logger.getLogger(getClass().getName())))

日志只包含以下内容:

javax.ws.rs.NotAuthorizedException: HTTP 401 Unauthorized

【问题讨论】:

  • 更改为:PAYLOAD_ANY。您之前看到的其他内容不是响应的文本。响应文本实际上只是您看到的内容,其他内容是详细日志记录。
  • @pandaadb 不,new LoggingFeature(Logger.getLogger(getClass().getName()), LoggingFeature.Verbosity.PAYLOAD_ANY) 产生完全相同的输出。根据文档PAYLOAD_TEXT 对应于(除其他外)内容类型application/json

标签: java json jersey dropwizard jersey-client


【解决方案1】:

这就是诀窍:

new LoggingFeature(Logger.getLogger(getClass().getName()), Level.OFF, LoggingFeature.Verbosity.PAYLOAD_TEXT, 8192)

我猜客户端中的日志记录功能类似于过滤器,而不是预期的包含。

【讨论】:

  • 不!你误会了。日志记录功能将记录每个请求,Verbosity 控制记录的内容。级别只是将日志条目放入记录器的级别。如果级别低于日志处理程序的级别,则处理程序将忽略日志条目。使用 Level.INFO 记录完全没问题,只需确保您的 LogHandler 不会忽略该级别。
【解决方案2】:

一个简短的例子来说明一个让开发人员认为日志记录功能不起作用的常见问题。

private static final LOG = Logger.getLogger(getClass().getName());

public void test() {
    Client client = ClientBuilder.newBuilder()
            .register(new LoggingFeature(LOG, Level.FINE, LoggingFeature.Verbosity.PAYLOAD_ALL, 8192))
            .build();
    // all requests and responses using this client will now be logged
    // with the log-level FINE to the logger LOG, but the logger
    // will simply ignore them, because it's default level is INFO
}

创建的记录器实例LOG使用默认的日志级别,即INFO。这意味着它将接受所有级别至少为 INFO 或更高(WARNING、SEVERE、...)的日志消息,但它将忽略所有级别较低的消息,例如 FINE。 (它仍然会将消息传递给它的父记录器,如果有的话)

注意:处理程序的默认日志级别为 Level.ALL,只要您不修改其级别,它们不应拒绝任何日志记录。

因此,您需要提高 LoggingFeatures 级别或降低 loggers 级别才能查看消息。

方案一:提高LoggingFeature的等级:

new LoggingFeature(LOG, Level.INFO, LoggingFeature.Verbosity.PAYLOAD_ALL, 8192)

解决方案2:降低Logger的级别:

LOG.setLevel(Level.FINE)

【讨论】:

    【解决方案3】:

    为了使用 LoggingFeature 记录服务器和客户端请求/响应的自定义日志,您需要创建一个扩展 LoggingFeature 并实现 ContainerRequestFilter、ContainerResponseFilter、ClientRequestFilter、ClientResponseFilter 和 WriterInterceptor 的自定义类。

    您需要覆盖 LoggingFeature 方法 public boolean configure(FeatureContext context) 并将您的 CustomLoggingFeature 对象注册到上下文中

    @Override
    public boolean configure(FeatureContext context) {
        context.register(this);
        return true;
    }
    

    ContainerRequestFilter 和 ContainerResponseFilter 接口具有用于记录服务器请求和响应的方法。

    ClientRequestFilter 和 ContainerResponseFilter 接口具有用于记录客户端请求和响应的方法。

    另外,您需要实现WriterInterceptor 来记录客户端请求和服务器响应正文。

    终于用球衣注册了你的 CustomLoggingFeature 类。

    environment.jersey().register(new CustomLoggingFeature(Logger.getLogger(getClass().getName()), Level.INFO, LoggingFeature.Verbosity.PAYLOAD_ANY, 8192));
    

    我正在使用 Dropwizard v1.3.5

    这是我的实现。

    public class CustomLoggingFeature extends LoggingFeature implements ContainerRequestFilter, ContainerResponseFilter,
        ClientRequestFilter, ClientResponseFilter, WriterInterceptor {
    
    private static final boolean printEntity = true;
    private static final int maxEntitySize = 8 * 1024;
    private final Logger logger = Logger.getLogger("CustomLoggingFeature");
    private static final String ENTITY_LOGGER_PROPERTY = CustomLoggingFeature.class.getName();
    private static final String NOTIFICATION_PREFIX = "* ";
    private static final String REQUEST_PREFIX = "> ";
    private static final String RESPONSE_PREFIX = "< ";
    private static final String AUTHORIZATION = "Authorization";
    private static final String EQUAL = " = ";
    private static final String HEADERS_SEPARATOR = ", ";
    private static List<String> requestHeaders;
    
    static {
        requestHeaders = new ArrayList<>();
        requestHeaders.add(AUTHORIZATION);
    }
    
    public CustomLoggingFeature(Logger logger, Level level, Verbosity verbosity, Integer maxEntitySize) {
        super(logger, level, verbosity, maxEntitySize);
    }
    
    @Override
    public boolean configure(FeatureContext context) {
        context.register(this);
        return true;
    }
    
    @Override
    public void filter(final ClientRequestContext context) {
        final StringBuilder b = new StringBuilder();
        printHeaders(b, context.getStringHeaders());
        printRequestLine(b, "Sending client request", context.getMethod(), context.getUri());
    
        if (printEntity && context.hasEntity()) {
            final OutputStream stream = new LoggingStream(b, context.getEntityStream());
            context.setEntityStream(stream);
            context.setProperty(ENTITY_LOGGER_PROPERTY, stream);
            // not calling log(b) here - it will be called by the interceptor
        } else {
            log(b);
        }
    }
    
    @Override
    public void filter(final ClientRequestContext requestContext, final ClientResponseContext responseContext) throws IOException {
        final StringBuilder b = new StringBuilder();
        printResponseLine(b, "Client response received", responseContext.getStatus());
    
        if (printEntity && responseContext.hasEntity()) {
            responseContext.setEntityStream(logInboundEntity(b, responseContext.getEntityStream(),
                    MessageUtils.getCharset(responseContext.getMediaType())));
        }
        log(b);
    }
    
    @Override
    public void filter(final ContainerRequestContext context) throws IOException {
        final StringBuilder b = new StringBuilder();
        printHeaders(b, context.getHeaders());
        printRequestLine(b, "Server has received a request", context.getMethod(), context.getUriInfo().getRequestUri());
    
        if (printEntity && context.hasEntity()) {
            context.setEntityStream(logInboundEntity(b, context.getEntityStream(), MessageUtils.getCharset(context.getMediaType())));
        }
        log(b);
    }
    
    @Override
    public void filter(final ContainerRequestContext requestContext, final ContainerResponseContext responseContext) {
        final StringBuilder b = new StringBuilder();
        printResponseLine(b, "Server responded with a response", responseContext.getStatus());
    
        if (printEntity && responseContext.hasEntity()) {
            final OutputStream stream = new LoggingStream(b, responseContext.getEntityStream());
            responseContext.setEntityStream(stream);
            requestContext.setProperty(ENTITY_LOGGER_PROPERTY, stream);
            // not calling log(b) here - it will be called by the interceptor
        } else {
            log(b);
        }
    }
    
    @Override
    public void aroundWriteTo(final WriterInterceptorContext writerInterceptorContext) throws IOException, WebApplicationException {
        final LoggingStream stream = (LoggingStream) writerInterceptorContext.getProperty(ENTITY_LOGGER_PROPERTY);
        writerInterceptorContext.proceed();
        if (stream != null) {
            log(stream.getStringBuilder(MessageUtils.getCharset(writerInterceptorContext.getMediaType())));
        }
    }
    
    private static class LoggingStream extends FilterOutputStream {
        private final StringBuilder b;
        private final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    
        LoggingStream(final StringBuilder b, final OutputStream inner) {
            super(inner);
    
            this.b = b;
        }
    
        StringBuilder getStringBuilder(Charset charset) {
            // write entity to the builder
            final byte[] entity = byteArrayOutputStream.toByteArray();
    
            b.append(new String(entity, 0, Math.min(entity.length, maxEntitySize), charset));
            if (entity.length > maxEntitySize) {
                b.append("...more...");
            }
            b.append('\n');
    
            return b;
        }
    
        public void write(final int i) throws IOException {
            if (byteArrayOutputStream.size() <= maxEntitySize) {
                byteArrayOutputStream.write(i);
            }
            out.write(i);
        }
    }
    
    private void printHeaders(StringBuilder b, MultivaluedMap<String, String> headers) {
        for (String header : requestHeaders) {
            if (Objects.nonNull(headers.get(header))) {
                b.append(header).append(EQUAL).append(headers.get(header)).append(HEADERS_SEPARATOR);
            }
        }
        int lastIndex = b.lastIndexOf(HEADERS_SEPARATOR);
        if (lastIndex != -1) {
            b.delete(lastIndex, lastIndex + HEADERS_SEPARATOR.length());
            b.append("\n");
        }
    }
    
    private void log(final StringBuilder b) {
        String message = Util.mask(b.toString());
        if (logger != null) {
            logger.info(message);
        }
    }
    
    private void printRequestLine(final StringBuilder b, final String note, final String method, final URI uri) {
        b.append(NOTIFICATION_PREFIX)
                .append(note)
                .append(" on thread ").append(Thread.currentThread().getId())
                .append(REQUEST_PREFIX).append(method).append(" ")
                .append(uri.toASCIIString()).append("\n");
    }
    
    private void printResponseLine(final StringBuilder b, final String note, final int status) {
        b.append(NOTIFICATION_PREFIX)
                .append(note)
                .append(" on thread ").append(Thread.currentThread().getId())
                .append(RESPONSE_PREFIX)
                .append(Integer.toString(status))
                .append("\n");
    }
    
    private InputStream logInboundEntity(final StringBuilder b, InputStream stream, final Charset charset) throws IOException {
        if (!stream.markSupported()) {
            stream = new BufferedInputStream(stream);
        }
        stream.mark(maxEntitySize + 1);
        final byte[] entity = new byte[maxEntitySize + 1];
        final int entitySize = stream.read(entity);
        b.append(new String(entity, 0, Math.min(entitySize, maxEntitySize), charset));
        if (entitySize > maxEntitySize) {
            b.append("...more...");
        }
        b.append('\n');
        stream.reset();
        return stream;
    }
    

    【讨论】:

      【解决方案4】:

      所以-我对此进行了测试。这可能是有关您的记录器配置和功能配置的问题,而不是实际功能。我给你的提示确实打印了你想要的,这是证明:

      public class MainClientTest {
      
          public static void main(String[] args) {
      
              Client build = ClientBuilder.newBuilder().build();
      
              // Configure Logger to log it all
              Logger logger = Logger.getLogger("test");
              logger.setLevel(Level.ALL);
              logger.setUseParentHandlers(false);
              Handler[] handlers = logger.getHandlers();
              for(Handler h : handlers) logger.removeHandler(h);
              logger.addHandler(buildseh());
              logger.info("Logger");
              build = build.register(new LoggingFeature(logger, Level.ALL, Verbosity.PAYLOAD_ANY, null));
      
              build.target("https://www.google.com").request().get();
          }
      
          public static StreamHandler buildseh() {
              final StreamHandler seh = new StreamHandler(System.err, new JdkLoggerFormatter()) {
                  @Override
                  public synchronized void publish(final LogRecord record) {
                      super.publish(record);
                      flush();
                  }
              };
              seh.setLevel(Level.ALL); // Default StdErr Setting
              return seh;
          }
      }
      

      这打印:

      1477055066111 I test Logger
      1477055066397   test 1 * Sending client request on thread main
      1 > GET https://www.google.com
      
      1477055067350   test 1 * Client response received on thread main
      1 < 200
      1 < Accept-Ranges: none
      1 < Alt-Svc: quic=":443"; ma=2592000; v="36,35,34,33,32"
      1 < Cache-Control: private, max-age=0
      1 < Content-Type: text/html; charset=ISO-8859-1
      1 < Date: Fri, 21 Oct 2016 13:04:27 GMT
      1 < Expires: -1
      1 < P3P: CP="This is not a P3P policy! See https://www.google.com/support/accounts/answer/151657?hl=en for more info."
      1 < Server: gws
      1 < Set-Cookie: NID=89=YPr3UcI5rcA4qiaXfm9zkA0uAWrnSDEbxN3TcFuhZ9PkLNvkSHBCHHLcYeXa7tNpzpM_9p7AFAreYq3kR9awqqKrhv5W6pWavfx5bZM7Jjbt559a4aEv20exEPJRmS1N; expires=Sat, 22-Apr-2017 13:04:27 GMT; path=/; domain=.google.co.uk; HttpOnly
      1 < Transfer-Encoding: chunked
      1 < Vary: Accept-Encoding
      1 < X-Frame-Options: SAMEORIGIN
      1 < X-XSS-Protection: 1; mode=block
      <!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="en-GB"><head><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"><meta content="/images/branding/googleg/1x/googleg_standard_color_128dp.png" itemprop="image"><title>Google</title><script>(function(){window.google={kEI:'WxIKWJOeEcGwa_nDmNgC',kEXPI:'1351633,1351901,3700318,3700400,4029815,4031109,4032678,4036527,4038012,4039268,4043492,4045841,4048347,4052304,4062185,4063220,4065786,4066665,4068550,4068816,4069839,4069841,4070140,4070329,4071229,4072287,4072364,4072602,4072705,4072773,4073248,4073405,4073758,4073913,4073959,4074684,4074809,4076096,4076315,4076930,4076999,4077037,4077119,4077
      

      所以,以下是陷阱:

      您的功能必须配置正确的级别。见:

      new LoggingFeature(logger, Level.ALL, Verbosity.PAYLOAD_ANY, null)
      

      必须将记录器配置为记录正确的内容。

      seh.setLevel(Level.ALL); // Default StdErr Setting
      logger.setLevel(Level.ALL);
      

      并且该功能必须具有正确的详细程度:Verbosity.PAYLOAD_ANY

      这就是你需要做的。

      问候,

      阿图尔

      【讨论】:

        【解决方案5】:

        对我来说,@l0b0 的解决方案确实记录了请求/响应负载,但我的访问日志将请求和响应记录为错误。以下行对我来说是正确的:

        new LoggingFeature(Logger.getLogger(getClass().getName()), Level.INFO, LoggingFeature.Verbosity.PAYLOAD_TEXT, 8192)
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2014-05-02
          • 2014-09-11
          • 1970-01-01
          • 2015-07-31
          • 2011-11-06
          • 2021-05-09
          • 2012-03-24
          • 1970-01-01
          相关资源
          最近更新 更多