HttpTrace of spring boot admin shows input and output participation monitoring Redisson

Posted by NargsBrood on Sun, 20 Oct 2019 11:12:51 +0200

Spring Boot admin (hereinafter referred to as SBA) integrates with Spring Boot and Spring cloud projects automatically in the way of starter, including Server side and Client side

SBA monitoring includes application basic information, logfile (online real-time browsing or download), JVM information (thread information, heap information, non heap information), Web(API interface information, information of the last 100 API calls), and user login information in the application. The monitoring indicators are comprehensive, but specific items need to be added, such as the following two points:

Custom HttpTrace add in and out parameters

Result:

The information displayed by HttpTrace in spring boot admin includes session, principle, request, response, timeTaken and timestamp, but session and principle are useless for the project. Request is the internal class display information of HttpTrace, including:

private final String method;
private final URI uri;
//The only place to expand
private final Map<String, List<String>> headers;
private final String remoteAddress;

response is also an internal class of HttpTrace:

private final int status;
//The only place to expand
private final Map<String, List<String>> headers;

The only thing missing is the input and output parameters of the request, and the Headers information is useless. Therefore, it is imperative to extend the input and output parameters in HttpTrace display request. The general idea is: convert the custom Filter -- > decoration mode to the custom request and response objects, get the request and corresponding content internally -- > httpexchangetracer creates HttpTrace object -- > inmemoryhttpttrace repository saves the HttpTrace object of 100 requests for the server. Because some of the objects used in Filter are created first, we start with the required components

  • Step 1: wrap HttpServletRequest to get the request content:
public class RequestWrapper extends HttpServletRequestWrapper {
//Store the message body of the request (CACHE one copy first)
    private byte[] body;
//Customize the wrapper class of the input stream to write the cached data to the stream again
    private ServletInputStreamWrapper wrapper;
    private final Logger logger = LoggerFactory.getLogger(RequestWrapper.class);

    public RequestWrapper(HttpServletRequest request) {
        super(request);
        try {
//Using Apache's commons IO tool to read data from request first
            body = IOUtils.toByteArray(request.getInputStream());
        } catch (IOException e) {
            logger.error("Exception getting request parameters from request:", e);
        }
//Write the read memory back to the stream
        wrapper = new ServletInputStreamWrapper(new ByteArrayInputStream(body));
    }
//Convert to String for external calls and replace escape characters
    public String body() {
        return new String(body).replaceAll("[\n\t\r]","");
    }
//Return our custom stream wrapper class for system call to read data
    @Override
    public ServletInputStream getInputStream() throws IOException {
        return this.wrapper;
    }
//Return our custom stream wrapper class for system call to read data
    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.wrapper));
    }
   //Read data from a given input stream
    static final class ServletInputStreamWrapper extends ServletInputStream {

        private InputStream inputStream;

        public ServletInputStreamWrapper(InputStream inputStream) {
            this.inputStream = inputStream;
        }

        @Override
        public boolean isFinished() {
            return true;
        }

        @Override
        public boolean isReady() {
            return false;
        }

        @Override
        public void setReadListener(ReadListener listener) {

        }
//Read cache data
        @Override
        public int read() throws IOException {
            return this.inputStream.read();
        }

        public InputStream getInputStream() {
            return inputStream;
        }

        public void setInputStream(InputStream inputStream) {
            this.inputStream = inputStream;
        }
    }
}
  • Step 2: wrap the HttpServletResponse class to get the response content:
public class ResponseWrapper extends HttpServletResponseWrapper {

    private HttpServletResponse response;
//Output stream of cached response content
    private ByteArrayOutputStream result = new ByteArrayOutputStream();

    public ResponseWrapper(HttpServletResponse response) {
        super(response);
        this.response = response;
    }

    /**
     * Response content for external calls
     *For large response content, oom is easy to occur (for example, / Actor / logfile interface). api filtering can be performed where the method is called.
     *The solution is in step 4
     */
    public String body(){
        return result.toString();
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return new ServletOutputStreamWrapper(this.response,this.result);
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        return new PrintWriter(new OutputStreamWriter(this.result,this.response.getCharacterEncoding()));
    }

//Inner class of wrapper class for custom output stream
    static final class ServletOutputStreamWrapper extends ServletOutputStream{

        private HttpServletResponse response;
        private ByteArrayOutputStream byteArrayOutputStream;

        public ServletOutputStreamWrapper(HttpServletResponse response, ByteArrayOutputStream byteArrayOutputStream) {
            this.response = response;
            this.byteArrayOutputStream = byteArrayOutputStream;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setWriteListener(WriteListener listener) {

        }

        @Override
        public void write(int b) throws IOException {
            this.byteArrayOutputStream.write(b);
        }

        /**
         * Refresh content back to the returned object and avoid multiple refreshes
         */
        @Override
        public void flush() throws IOException {
            if(!response.isCommitted()){
                byte[] bytes = this.byteArrayOutputStream.toByteArray();
                ServletOutputStream outputStream = response.getOutputStream();
                outputStream.write(bytes);
                outputStream.flush();
            }
        }
    }
}
  • Step 3: expand TraceableRequest. The methods in this interface will be called when creating the httptrace ා Request internal class. The methods in the custom implementation can be used, and then the class can be referenced in the filter to achieve the purpose of custom display content. The Request in this class is the decoration class we created in step 1. HttpServletRequest cannot be used.
public class CustomerTraceableRequest implements TraceableRequest {
//Custom Request decoration class, cannot use HttpServletRequest
    private RequestWrapper request;

    public CustomerTraceableRequest(RequestWrapper request) {
        this.request = request;
    }
//getMethod in HttpTrace class will call
    @Override
    public String getMethod() {
        return request.getMethod();
    }

    /**
     * @return POST Or GET returns {ip}:{port}/uir
     */
    @Override
    public URI getUri() {
        return URI.create(request.getRequestURL().toString());
    }

//Because the only extensible Map in HttpTrace is the header Map, our custom attribute RequestParam is stored in the headers as the input information.
    @Override
    public Map<String, List<String>> getHeaders() {
        Map<String, List<String>> headerParam = new HashMap<>(1);
        headerParam.put("RequestParam",getParams());
        return headerParam;
    }

//This method also needs to be rewritten. The default is too simple to get the real IP address.
    @Override
    public String getRemoteAddress() {
        return IpUtils.getIpAddress(request);
    }
//According to the different request methods of GET or POST, GET the request parameters in different situations
    public List<String> getParams() {
        String params = null;
        String method = this.getMethod();
        if(HttpMethod.GET.matches(method)){
            params = request.getQueryString();
        }else if(HttpMethod.POST.matches(method)){
            params = this.request.body();
        }
        List<String> result = new ArrayList<>(1);
        result.add(params);
        return result;
    }
}
  • Step 4: expand TraceableResponse. The methods in this interface are referenced when creating the httptrace ා response internal class. Customize the methods in the implementation:
public class CustomerTraceableResponse implements TraceableResponse {
    //Custom HttpServletResponse wrapper class
    private ResponseWrapper response;
    private HttpServletRequest request;

    public CustomerTraceableResponse(ResponseWrapper response, HttpServletRequest request) {
        this.response = response;
        this.request = request;
    }
//Return to response status
    @Override
    public int getStatus() {
        return response.getStatus();
    }
//Expand the Response headers to add the Response Body attribute to display the response content, but you need to exclude the requests beginning with '/ Actor /', some of which are too large and easy to OOM.
    @Override
    public Map<String, List<String>> getHeaders() {
        if(isActuatorUri()){
            return extractHeaders();
        }else{
            Map<String, List<String>> result = new LinkedHashMap<>(1);
            List<String> responseBody = new ArrayList<>(1);
            responseBody.add(this.response.body());
            result.put("ResponseBody", responseBody);
            result.put("Content-Type", getContentType());
            return result;
        }
    }
//Whether it is the request uri to be filtered
    private boolean isActuatorUri() {
        String requestUri = request.getRequestURI();
        AntPathMatcher matcher = new AntPathMatcher();
        return matcher.match("/actuator/**", requestUri);
    }
//The content type and Length displayed on the server-side page are obtained from the Response
    private List<String> getContentType() {
        List<String> list = new ArrayList<>(1);
        list.add(this.response.getContentType());
        return list;
    }
//For the request of / Actor / *, return the default headers content.
    private Map<String, List<String>> extractHeaders() {
        Map<String, List<String>> headers = new LinkedHashMap<>();
        for (String name : this.response.getHeaderNames()) {
            headers.put(name, new ArrayList<>(this.response.getHeaders(name)));
        }
        return headers;
    }
}
  • Step 5: Customize Filter to Filter Response and Response, and create HttpTrace object:
public class CustomerHttpTraceFilter extends OncePerRequestFilter implements Ordered {
//The repository that stores HttpTrace is memory by default, which can extend the way that this class stores data after swapping.
    private HttpTraceRepository httpTraceRepository;
//This class creates HttpTrace objects. Set < include > contains the containers (request headers, response headers, remote address, time token) that we need to display those contents.
    private HttpExchangeTracer httpExchangeTracer;

    public CustomerHttpTraceFilter(HttpTraceRepository httpTraceRepository, HttpExchangeTracer httpExchangeTracer) {
        this.httpTraceRepository = httpTraceRepository;
        this.httpExchangeTracer = httpExchangeTracer;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//Verify that the URI is valid
        if (!isRequestValid(request)) {
            filterChain.doFilter(request, response);
            return;
        }
//Package HttpServletRequest as our own
        RequestWrapper wrapper = new RequestWrapper(request);
//Package HttpServletResponse as our own
        ResponseWrapper responseWrapper = new ResponseWrapper(response);

//Create our own TraceRequest object
        CustomerTraceableRequest traceableRequest = new CustomerTraceableRequest(wrapper);
//Create an HttpTrace object (filterdtraceablerequest is an internal class. Filter the information to be displayed and save it through set < include >), and focus on setting various parameters of the HttpTrace ා request object.
        HttpTrace httpTrace = httpExchangeTracer.receivedRequest(traceableRequest);
        try {
            filterChain.doFilter(wrapper, responseWrapper);
        } finally {
//Customized TraceableResponse saves the required response information
            CustomerTraceableResponse traceableResponse = new CustomerTraceableResponse(responseWrapper,request);
//Set the session, principal, timetoken information and Response internal class information in HttpTrace according to set < include >.       
  this.httpExchangeTracer.sendingResponse(httpTrace, traceableResponse, null, null);
//Save the HttpTrace object in the repository
            this.httpTraceRepository.add(httpTrace);
        }
    }

    private boolean isRequestValid(HttpServletRequest request) {
        try {
            new URI(request.getRequestURL().toString());
            return true;
        } catch (URISyntaxException ex) {
            return false;
        }
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE - 10;
    }
}
  • Step 6: disable automatic configuration of HttpTraceAutoConfiguration through @ SpringBootApplication(exclude), customize automatic configuration and replace Filter filter:
@Configuration
@ConditionalOnWebApplication
@ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
@EnableConfigurationProperties(HttpTraceProperties.class)
public class TraceFilterConfig {

//Objects storing HttpTrace information
    @Bean
    @ConditionalOnMissingBean(HttpTraceRepository.class)
    public InMemoryHttpTraceRepository traceRepository() {
        return new InMemoryHttpTraceRepository();
    }
//Create HttpTrace object Exchange
    @Bean
    @ConditionalOnMissingBean
    public HttpExchangeTracer httpExchangeTracer(HttpTraceProperties traceProperties) {
        return new HttpExchangeTracer(traceProperties.getInclude());
    }

    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
    static class ServletTraceFilterConfiguration {
//Register our customized Filter in Bean mode to take effect
        @Bean
        @ConditionalOnMissingBean
        public CustomerHttpTraceFilter httpTraceFilter(HttpTraceRepository repository,
                                               HttpExchangeTracer tracer) {
            return new CustomerHttpTraceFilter(repository,tracer);
        }
    }

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

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

Integrated Redisson health status monitoring

If spring boot starter Redis is introduced, SBA will monitor the health status of Redis by default with RedisConnectionFactory, but Redisson has not yet done so, so it has plenty of food and clothing. Health monitoring of different components is realized by using the policy mode of HealthIndicator and ReactiveHealthIndicator. The latter uses the recive mode. I configure Redisson in the way of JavaBean, so by the way, implement the reactivehealth indicator and add the indicator:

@Configuration
@EnableConfigurationProperties(value = RedissonProperties.class)
public class RedissonConfig implements ReactiveHealthIndicator {
//Own redisson properties file
    @Autowired
    private RedissonProperties redissonProperties;
//Expose redissonClient handle
    @Bean
    @ConditionalOnMissingBean
    public RedissonClient redisClient() {
        return Redisson.create(config());
    }
//Configure RedissonConfig information by Bean
    @Bean
    public Config config() {
        Config config = new Config();
        config.useSingleServer() //Single real mode
                .setAddress(redissonProperties.getAddress() + ":" + redissonProperties.getPort())
                .setPassword(redissonProperties.getPassword())
                .setDatabase(redissonProperties.getDatabase())
                .setConnectionPoolSize(redissonProperties.getConnectionPoolSize())
                .setConnectionMinimumIdleSize(redissonProperties.getConnectionMinimumIdleSize())
                .setIdleConnectionTimeout(redissonProperties.getIdleConnectionTimeout())
                .setSubscriptionConnectionPoolSize(redissonProperties.getSubscriptionConnectionPoolSize())
                .setSubscriptionConnectionMinimumIdleSize(redissonProperties.getSubscriptionConnectionMinimumIdleSize())
                .setTimeout(redissonProperties.getTimeout())
                .setRetryAttempts(redissonProperties.getRetryAttempts())
                .setRetryInterval(redissonProperties.getRetryInterval())
                .setConnectTimeout(redissonProperties.getConnectTimeout())
                .setReconnectionTimeout(redissonProperties.getReconnectionTimeout());
        config
                .setCodecProvider(new DefaultCodecProvider())
                .setEventLoopGroup(new NioEventLoopGroup())
                .setThreads(Runtime.getRuntime().availableProcessors() * 2)
                .setNettyThreads(Runtime.getRuntime().availableProcessors() * 2);
        return config;
    }
//Implement reactivehealth indicator to override health method
    @Override
    public Mono<Health> health() {
        return checkRedissonHealth().onErrorResume(ex -> Mono.just(new Health.Builder().down(ex).build()));
    }
//I determine whether the redis server is up by ping, and add Netty and Threads monitoring.
    private Mono<Health> checkRedissonHealth() {
        Health.Builder builder = new Health.Builder();
        builder.withDetail("address", redissonProperties.getAddress());
        //Check health status
        if (this.redisClient().getNodesGroup().pingAll()) {
            builder.status(Status.UP);
            builder.withDetail("dataBase", redissonProperties.getDatabase());
            builder.withDetail("redisNodeThreads", this.redisClient().getConfig().getThreads());
            builder.withDetail("nettyThreads", this.redisClient().getConfig().getNettyThreads());

        }else{
            builder.status(Status.DOWN);
        }
        return Mono.just(builder.build());
    }
}

On the page:

Ok! Complete!
If there is any mistake, please give me some advice!

Welcome to the public account:

Topics: Java Spring Session Redis Attribute