如何在 Dropwizard 1.0.2 中使用 LoggingFeature 打印服务器响应?

How to print server responses using LoggingFeature in Dropwizard 1.0.2?

以下代码导致在 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

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

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

这就是您需要做的。

此致,

阿图尔

这样就可以了:

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

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

对我来说,@l0b0 的解决方案确实记录了 request/response 有效载荷,但我的访问日志将请求和响应记录为错误。以下行对我来说工作正常:

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

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

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 级别或降低记录器级别才能看到消息。

解决方案 1:提高 LoggingFeature 的级别:

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

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

LOG.setLevel(Level.FINE)

要使用 LoggingFeature 为服务器和客户端 request/response 记录自定义日志,您需要创建一个扩展 LoggingFeature 的自定义 class 并实现 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 class。

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;
}