Talk about httptrace for springboot2

  springboot

Order

This paper mainly studies the httptrace of springboot2.

HttpTraceAutoConfiguration

spring-boot-actuator-autoconfigure-2.0.1.RELEASE-sources.jar! /org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceAutoConfiguration.java

@Configuration
@ConditionalOnWebApplication
@ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
@EnableConfigurationProperties(HttpTraceProperties.class)
public class HttpTraceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(HttpTraceRepository.class)
    public InMemoryHttpTraceRepository traceRepository() {
        return new InMemoryHttpTraceRepository();
    }

    @Bean
    @ConditionalOnMissingBean
    public HttpExchangeTracer httpExchangeTracer(HttpTraceProperties traceProperties) {
        return new HttpExchangeTracer(traceProperties.getInclude());
    }

    @ConditionalOnWebApplication(type = Type.SERVLET)
    static class ServletTraceFilterConfiguration {

        @Bean
        @ConditionalOnMissingBean
        public HttpTraceFilter httpTraceFilter(HttpTraceRepository repository,
                HttpExchangeTracer tracer) {
            return new HttpTraceFilter(repository, tracer);
        }

    }

    @ConditionalOnWebApplication(type = Type.REACTIVE)
    static class ReactiveTraceFilterConfiguration {

        @Bean
        @ConditionalOnMissingBean
        public HttpTraceWebFilter httpTraceWebFilter(HttpTraceRepository repository,
                HttpExchangeTracer tracer, HttpTraceProperties traceProperties) {
            return new HttpTraceWebFilter(repository, tracer,
                    traceProperties.getInclude());
        }

    }

}

It can be seen here that different filter are injected in servlet and reactive respectively, the servlet is HttpTraceFilter, and the reactive is HttpTraceWebFilter
Both servlet and reactive methods inject HttpTraceRepository and HttpExchangeTracer.

InMemoryHttpTraceRepository

spring-boot-actuator-2.0.1.RELEASE-sources.jar! /org/springframework/boot/actuate/trace/http/InMemoryHttpTraceRepository.java

public class InMemoryHttpTraceRepository implements HttpTraceRepository {

    private int capacity = 100;

    private boolean reverse = true;

    private final List<HttpTrace> traces = new LinkedList<>();

    /**
     * Flag to say that the repository lists traces in reverse order.
     * @param reverse flag value (default true)
     */
    public void setReverse(boolean reverse) {
        synchronized (this.traces) {
            this.reverse = reverse;
        }
    }

    /**
     * Set the capacity of the in-memory repository.
     * @param capacity the capacity
     */
    public void setCapacity(int capacity) {
        synchronized (this.traces) {
            this.capacity = capacity;
        }
    }

    @Override
    public List<HttpTrace> findAll() {
        synchronized (this.traces) {
            return Collections.unmodifiableList(new ArrayList<>(this.traces));
        }
    }

    @Override
    public void add(HttpTrace trace) {
        synchronized (this.traces) {
            while (this.traces.size() >= this.capacity) {
                this.traces.remove(this.reverse ? this.capacity - 1 : 0);
            }
            if (this.reverse) {
                this.traces.add(0, trace);
            }
            else {
                this.traces.add(trace);
            }
        }
    }

}

The default setting here is to store only the latest 100 requests, otherwise this one uses synchronized, which seems not very efficient.

HttpExchangeTracer

spring-boot-actuator-2.0.1.RELEASE-sources.jar! /org/springframework/boot/actuate/trace/http/HttpExchangeTracer.java

public class HttpExchangeTracer {

    private final Set<Include> includes;

    /**
     * Creates a new {@code HttpExchangeTracer} that will use the given {@code includes}
     * to determine the contents of its traces.
     * @param includes the includes
     */
    public HttpExchangeTracer(Set<Include> includes) {
        this.includes = includes;
    }

    /**
     * Begins the tracing of the exchange that was initiated by the given {@code request}
     * being received.
     * @param request the received request
     * @return the HTTP trace for the
     */
    public final HttpTrace receivedRequest(TraceableRequest request) {
        return new HttpTrace(new FilteredTraceableRequest(request));
    }

    /**
     * Ends the tracing of the exchange that is being concluded by sending the given
     * {@code response}.
     * @param trace the trace for the exchange
     * @param response the response that concludes the exchange
     * @param principal a supplier for the exchange's principal
     * @param sessionId a supplier for the id of the exchange's session
     */
    public final void sendingResponse(HttpTrace trace, TraceableResponse response,
            Supplier<Principal> principal, Supplier<String> sessionId) {
        setIfIncluded(Include.TIME_TAKEN,
                () -> System.currentTimeMillis() - trace.getTimestamp().toEpochMilli(),
                trace::setTimeTaken);
        setIfIncluded(Include.SESSION_ID, sessionId, trace::setSessionId);
        setIfIncluded(Include.PRINCIPAL, principal, trace::setPrincipal);
        trace.setResponse(
                new HttpTrace.Response(new FilteredTraceableResponse(response)));
    }
    //......
}

HttpExchangetracer is a simple tracer. It is mainly the receivedRequest method that records the request to generate HttpTrace. Then sendingResponse ends the Trace and adds the record to HttpTrace.

Filter

HttpTraceFilter

spring-boot-actuator-2.0.1.RELEASE-sources.jar! /org/springframework/boot/actuate/web/trace/servlet/HttpTraceFilter.java

public class HttpTraceFilter extends OncePerRequestFilter implements Ordered {

    // Not LOWEST_PRECEDENCE, but near the end, so it has a good chance of catching all
    // enriched headers, but users can add stuff after this if they want to
    private int order = Ordered.LOWEST_PRECEDENCE - 10;

    private final HttpTraceRepository repository;

    private final HttpExchangeTracer tracer;

    /**
     * Create a new {@link HttpTraceFilter} instance.
     * @param repository the trace repository
     * @param tracer used to trace exchanges
     */
    public HttpTraceFilter(HttpTraceRepository repository, HttpExchangeTracer tracer) {
        this.repository = repository;
        this.tracer = tracer;
    }

    @Override
    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        TraceableHttpServletRequest traceableRequest = new TraceableHttpServletRequest(
                request);
        HttpTrace trace = this.tracer.receivedRequest(traceableRequest);
        int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
        try {
            filterChain.doFilter(request, response);
            status = response.getStatus();
        }
        finally {
            TraceableHttpServletResponse traceableResponse = new TraceableHttpServletResponse(
                    status == response.getStatus() ? response
                            : new CustomStatusResponseWrapper(response, status));
            this.tracer.sendingResponse(trace, traceableResponse,
                    request::getUserPrincipal, () -> getSessionId(request));
            this.repository.add(trace);
        }
    }

    private String getSessionId(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        return session == null ? null : session.getId();
    }

    private static final class CustomStatusResponseWrapper
            extends HttpServletResponseWrapper {

        private final int status;

        private CustomStatusResponseWrapper(HttpServletResponse response, int status) {
            super(response);
            this.status = status;
        }

        @Override
        public int getStatus() {
            return this.status;
        }

    }

}

You can see that the inheritance is OncePerRequestFilter.

HttpTraceWebFilter

spring-boot-actuator-2.0.1.RELEASE-sources.jar! /org/springframework/boot/actuate/web/trace/reactive/HttpTraceWebFilter.java

public class HttpTraceWebFilter implements WebFilter, Ordered {

    private static final Object NONE = new Object();

    // Not LOWEST_PRECEDENCE, but near the end, so it has a good chance of catching all
    // enriched headers, but users can add stuff after this if they want to
    private int order = Ordered.LOWEST_PRECEDENCE - 10;

    private final HttpTraceRepository repository;

    private final HttpExchangeTracer tracer;

    private final Set<Include> includes;

    public HttpTraceWebFilter(HttpTraceRepository repository, HttpExchangeTracer tracer,
            Set<Include> includes) {
        this.repository = repository;
        this.tracer = tracer;
        this.includes = includes;
    }

    @Override
    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        Mono<?> principal = this.includes.contains(Include.PRINCIPAL)
                ? exchange.getPrincipal().cast(Object.class).defaultIfEmpty(NONE)
                : Mono.just(NONE);
        Mono<?> session = this.includes.contains(Include.SESSION_ID)
                ? exchange.getSession() : Mono.just(NONE);
        return Mono.zip(principal, session)
                .flatMap((tuple) -> filter(exchange, chain,
                        asType(tuple.getT1(), Principal.class),
                        asType(tuple.getT2(), WebSession.class)));
    }

    private <T> T asType(Object object, Class<T> type) {
        if (type.isInstance(object)) {
            return type.cast(object);
        }
        return null;
    }

    private Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain,
            Principal principal, WebSession session) {
        ServerWebExchangeTraceableRequest request = new ServerWebExchangeTraceableRequest(
                exchange);
        HttpTrace trace = this.tracer.receivedRequest(request);
        return chain.filter(exchange).doAfterSuccessOrError((aVoid, ex) -> {
            this.tracer.sendingResponse(trace,
                    new TraceableServerHttpResponse(ex == null ? exchange.getResponse()
                            : new CustomStatusResponseDecorator(ex,
                                    exchange.getResponse())),
                    () -> principal, () -> getStartedSessionId(session));
            this.repository.add(trace);
        });
    }

    private String getStartedSessionId(WebSession session) {
        return (session != null && session.isStarted()) ? session.getId() : null;
    }

    private static final class CustomStatusResponseDecorator
            extends ServerHttpResponseDecorator {

        private final HttpStatus status;

        private CustomStatusResponseDecorator(Throwable ex, ServerHttpResponse delegate) {
            super(delegate);
            this.status = ex instanceof ResponseStatusException
                    ? ((ResponseStatusException) ex).getStatus()
                    : HttpStatus.INTERNAL_SERVER_ERROR;
        }

        @Override
        public HttpStatus getStatusCode() {
            return this.status;
        }

    }

}

You can see that the inheritance is WebFilter.

Endpoint

HttpTraceEndpointAutoConfiguration

spring-boot-actuator-autoconfigure-2.0.1.RELEASE-sources.jar! /org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceEndpointAutoConfiguration.java

@Configuration
@AutoConfigureAfter(HttpTraceAutoConfiguration.class)
public class HttpTraceEndpointAutoConfiguration {

    @Bean
    @ConditionalOnBean(HttpTraceRepository.class)
    @ConditionalOnMissingBean
    @ConditionalOnEnabledEndpoint
    public HttpTraceEndpoint httpTraceEndpoint(HttpTraceRepository traceRepository) {
        return new HttpTraceEndpoint(traceRepository);
    }

}

HttpTraceEndpoint was created here using traceRepository.

HttpTraceEndpoint

spring-boot-actuator-2.0.1.RELEASE-sources.jar! /org/springframework/boot/actuate/trace/http/HttpTraceEndpoint.java

/**
 * {@link Endpoint} to expose {@link HttpTrace} information.
 *
 * @author Dave Syer
 * @author Andy Wilkinson
 * @since 2.0.0
 */
@Endpoint(id = "httptrace")
public class HttpTraceEndpoint {

    private final HttpTraceRepository repository;

    /**
     * Create a new {@link HttpTraceEndpoint} instance.
     * @param repository the trace repository
     */
    public HttpTraceEndpoint(HttpTraceRepository repository) {
        Assert.notNull(repository, "Repository must not be null");
        this.repository = repository;
    }

    @ReadOperation
    public HttpTraceDescriptor traces() {
        return new HttpTraceDescriptor(this.repository.findAll());
    }

    /**
     * A description of an application's {@link HttpTrace} entries. Primarily intended for
     * serialization to JSON.
     */
    public static final class HttpTraceDescriptor {

        private final List<HttpTrace> traces;

        private HttpTraceDescriptor(List<HttpTrace> traces) {
            this.traces = traces;
        }

        public List<HttpTrace> getTraces() {
            return this.traces;
        }

    }

}

Output instance

{
  "traces": [
    {
      "timestamp": "2018-04-21T14:14:36.256Z",
      "principal": null,
      "session": null,
      "request": {
        "method": "GET",
        "uri": "http://localhost:8080/actuator",
        "headers": {
          "host": [
            "localhost:8080"
          ],
          "connection": [
            "keep-alive"
          ],
          "upgrade-insecure-requests": [
            "1"
          ],
          "user-agent": [
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36"
          ],
          "accept": [
            "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"
          ],
          "accept-encoding": [
            "gzip, deflate, br"
          ],
          "accept-language": [
            "zh-CN,zh;q=0.9,en;q=0.8"
          ],
          "cookie": [
            "hibext_instdsigdipv2=1; _ga=GA1.1.933052261.1524234775; _gid=GA1.1.1398833521.1524234775"
          ]
        },
        "remoteAddress": null
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": [
            "application/vnd.spring-boot.actuator.v2+json;charset=UTF-8"
          ],
          "Transfer-Encoding": [
            "chunked"
          ],
          "Date": [
            "Sat, 21 Apr 2018 14:14:36 GMT"
          ]
        }
      },
      "timeTaken": 110
    }
  ]
}

Summary

Httptrace is on by default, and different filters are used for servlet and reactive methods respectively. The former is HttpTraceFilter that inherits OncePerRequestFilter, and the latter is HttpTraceWebFilter that inherits WebFilter. InMemoryHttpTraceRepository can be replaced by customization, and the request log can be delivered to the log center asynchronously by itself.

doc