spring-boot-admin(以下简称SBA)与Spring Boot、Spring cloud项目以starter得方式自动集成,包括Server端和Client端
SBA监控包括应用的基本信息、logfile(在线实时浏览或者download)、JVM信息(线程信息、堆信息、非堆信息)、Web(API接口信息、最近100次API调用的信息)、应用中用户登录信息;监控指标很全面,但针对具体项目就要增加符合自己项目的内容了,比如如下两点:
自定义HttpTrace增加入参和出参
结果:
在spring-boot-admin中HttpTrace显示的信息包括session、principal、request、response、timeTaken和timestamp,但session、principal对该项目完全无用,request是HttpTrace的内部类显示信息包括:
1private final String method;
2private final URI uri;
3//唯一可以扩展的地方
4private final Map<String, List<String>> headers;
5private final String remoteAddress;
response也是HttpTrace的内部类:
1private final int status;
2//唯一可以扩展的地方
3private final Map<String, List<String>> headers;
唯一缺少的就是请求的入参和出参,而Headers的信息是无用的。所以扩展HttpTrace显示请求中的入参和出参势在必行,大致的思路是:自定义Filter-->装饰模式转换成自定义的request和response对象,内部获取请求和相应内容-->HttpExchangeTracer创建HttpTrace对象-->InmemoryHttpTraceRepository保存100次请求的HttpTrace对象,供server端使用。由于Filter中使用的部分对象是先创建的所以我们先从需要的零部件开始
- 第一步:包装HttpServletRequest获取请求内容:
1public class RequestWrapper extends HttpServletRequestWrapper {
2//存放请求的消息体(先缓存一份)
3 private byte[] body;
4//自定义输入流的包装类,将缓存数据再写入到流中
5 private ServletInputStreamWrapper wrapper;
6 private final Logger logger = LoggerFactory.getLogger(RequestWrapper.class);
7
8 public RequestWrapper(HttpServletRequest request) {
9 super(request);
10 try {
11//使用Apache的commons-io工具从request中先读取数据
12 body = IOUtils.toByteArray(request.getInputStream());
13 } catch (IOException e) {
14 logger.error("从请求中获取请求参数出现异常:", e);
15 }
16//将读取出来的内存再写入流中
17 wrapper = new ServletInputStreamWrapper(new ByteArrayInputStream(body));
18 }
19//转换成String 供外部调用,并替换转义字符
20 public String body() {
21 return new String(body).replaceAll("[\n\t\r]","");
22 }
23//将我们的自定义的流包装类返回,供系统调用 读取数据
24 @Override
25 public ServletInputStream getInputStream() throws IOException {
26 return this.wrapper;
27 }
28//将我们的自定义的流包装类返回,供系统调用 读取数据
29 @Override
30 public BufferedReader getReader() throws IOException {
31 return new BufferedReader(new InputStreamReader(this.wrapper));
32 }
33 //从给定的输入流中读取数据
34 static final class ServletInputStreamWrapper extends ServletInputStream {
35
36 private InputStream inputStream;
37
38 public ServletInputStreamWrapper(InputStream inputStream) {
39 this.inputStream = inputStream;
40 }
41
42 @Override
43 public boolean isFinished() {
44 return true;
45 }
46
47 @Override
48 public boolean isReady() {
49 return false;
50 }
51
52 @Override
53 public void setReadListener(ReadListener listener) {
54
55 }
56//读取缓存数据
57 @Override
58 public int read() throws IOException {
59 return this.inputStream.read();
60 }
61
62 public InputStream getInputStream() {
63 return inputStream;
64 }
65
66 public void setInputStream(InputStream inputStream) {
67 this.inputStream = inputStream;
68 }
69 }
70}
- 第二步:包装HttpServletResponse类获取响应内容:
1public class ResponseWrapper extends HttpServletResponseWrapper {
2
3 private HttpServletResponse response;
4//缓存响应内容的输出流
5 private ByteArrayOutputStream result = new ByteArrayOutputStream();
6
7 public ResponseWrapper(HttpServletResponse response) {
8 super(response);
9 this.response = response;
10 }
11
12 /**
13 * 响应的内容 供外部调用
14 *针对 体积较大的响应内容 很容易发生 OOM(比如:/actuator/logfile 接口),可在调用该方法的地方就行api过滤
15 *解决方法在第四步
16 */
17 public String body(){
18 return result.toString();
19 }
20
21 @Override
22 public ServletOutputStream getOutputStream() throws IOException {
23 return new ServletOutputStreamWrapper(this.response,this.result);
24 }
25
26 @Override
27 public PrintWriter getWriter() throws IOException {
28 return new PrintWriter(new OutputStreamWriter(this.result,this.response.getCharacterEncoding()));
29 }
30
31//自定义输出流的包装类 内部类
32 static final class ServletOutputStreamWrapper extends ServletOutputStream{
33
34 private HttpServletResponse response;
35 private ByteArrayOutputStream byteArrayOutputStream;
36
37 public ServletOutputStreamWrapper(HttpServletResponse response, ByteArrayOutputStream byteArrayOutputStream) {
38 this.response = response;
39 this.byteArrayOutputStream = byteArrayOutputStream;
40 }
41
42 @Override
43 public boolean isReady() {
44 return true;
45 }
46
47 @Override
48 public void setWriteListener(WriteListener listener) {
49
50 }
51
52 @Override
53 public void write(int b) throws IOException {
54 this.byteArrayOutputStream.write(b);
55 }
56
57 /**
58 * 将内容重新刷新到返回的对象中 并且避免多次刷新
59 */
60 @Override
61 public void flush() throws IOException {
62 if(!response.isCommitted()){
63 byte[] bytes = this.byteArrayOutputStream.toByteArray();
64 ServletOutputStream outputStream = response.getOutputStream();
65 outputStream.write(bytes);
66 outputStream.flush();
67 }
68 }
69 }
70}
- 第三步:扩展
TraceableRequest,该接口中的方法会在创建HttpTrace#Request内部类时调用,自定义实现里面的方法,再在过滤器中引用该类就可以达到自定义显示内容的目的,该类中的Request是我们第一步创建的装饰类,不能使用HttpServletRequest
1public class CustomerTraceableRequest implements TraceableRequest {
2//自定义的Request装饰类,不能使用HttpServletRequest
3 private RequestWrapper request;
4
5 public CustomerTraceableRequest(RequestWrapper request) {
6 this.request = request;
7 }
8//HttpTrace类中getMethod会调用
9 @Override
10 public String getMethod() {
11 return request.getMethod();
12 }
13
14 /**
15 * @return POST 或者 GET 方式 都返回 {ip}:{port}/uir的形式返回
16 */
17 @Override
18 public URI getUri() {
19 return URI.create(request.getRequestURL().toString());
20 }
21
22//因为在HttpTrace中可扩展的只有headers的Map,所以我们自定义属性RequestParam存入headers中,作为入参信息展示
23 @Override
24 public Map<String, List<String>> getHeaders() {
25 Map<String, List<String>> headerParam = new HashMap<>(1);
26 headerParam.put("RequestParam",getParams());
27 return headerParam;
28 }
29
30//该方法也要重写,默认的太简单无法获取真是的IP
31 @Override
32 public String getRemoteAddress() {
33 return IpUtils.getIpAddress(request);
34 }
35//根据GET或者POST的请求方式不同,获取不同情况下的请求参数
36 public List<String> getParams() {
37 String params = null;
38 String method = this.getMethod();
39 if(HttpMethod.GET.matches(method)){
40 params = request.getQueryString();
41 }else if(HttpMethod.POST.matches(method)){
42 params = this.request.body();
43 }
44 List<String> result = new ArrayList<>(1);
45 result.add(params);
46 return result;
47 }
48}
- 第四步:扩展
TraceableResponse,该接口中方法在创建HttpTrace#Response内部类时引用,自定义实现里面的方法:
1public class CustomerTraceableResponse implements TraceableResponse {
2 //自定义的HttpServletResponse包装类
3 private ResponseWrapper response;
4 private HttpServletRequest request;
5
6 public CustomerTraceableResponse(ResponseWrapper response, HttpServletRequest request) {
7 this.response = response;
8 this.request = request;
9 }
10//返回响应状态
11 @Override
12 public int getStatus() {
13 return response.getStatus();
14 }
15//扩展Response headers添加Response Body属性,展示响应内容,但是需要排除`/actuator/`开头的请求,这里面部分响应内容太大,容易OOM
16 @Override
17 public Map<String, List<String>> getHeaders() {
18 if(isActuatorUri()){
19 return extractHeaders();
20 }else{
21 Map<String, List<String>> result = new LinkedHashMap<>(1);
22 List<String> responseBody = new ArrayList<>(1);
23 responseBody.add(this.response.body());
24 result.put("ResponseBody", responseBody);
25 result.put("Content-Type", getContentType());
26 return result;
27 }
28 }
29//是否是需要过滤的请求uri
30 private boolean isActuatorUri() {
31 String requestUri = request.getRequestURI();
32 AntPathMatcher matcher = new AntPathMatcher();
33 return matcher.match("/actuator/**", requestUri);
34 }
35//server端页面展示的Content-Type以及Length是从Response中获取的
36 private List<String> getContentType() {
37 List<String> list = new ArrayList<>(1);
38 list.add(this.response.getContentType());
39 return list;
40 }
41//针对/actuator/**的请求返回默认的headers内容获
42 private Map<String, List<String>> extractHeaders() {
43 Map<String, List<String>> headers = new LinkedHashMap<>();
44 for (String name : this.response.getHeaderNames()) {
45 headers.put(name, new ArrayList<>(this.response.getHeaders(name)));
46 }
47 return headers;
48 }
49}
- 第五步:自定义
Filter对Resquest和Response过滤,并创建HttpTrace对象:
1public class CustomerHttpTraceFilter extends OncePerRequestFilter implements Ordered {
2//存储HttpTrace的repository,默认是居于内存的,可扩展该类跟换存储数据的方式
3 private HttpTraceRepository httpTraceRepository;
4//该类创建HttpTrace对象,Set<Include>包含的内容是我们需要展示那些内容的容器(request-headers,response-headers,remote-address,time-taken)
5 private HttpExchangeTracer httpExchangeTracer;
6
7 public CustomerHttpTraceFilter(HttpTraceRepository httpTraceRepository, HttpExchangeTracer httpExchangeTracer) {
8 this.httpTraceRepository = httpTraceRepository;
9 this.httpExchangeTracer = httpExchangeTracer;
10 }
11
12 @Override
13 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
14//校验URI是否有效
15 if (!isRequestValid(request)) {
16 filterChain.doFilter(request, response);
17 return;
18 }
19//将HttpServletRequest包装成我们自己的
20 RequestWrapper wrapper = new RequestWrapper(request);
21//将HttpServletResponse包装成我们的自己的
22 ResponseWrapper responseWrapper = new ResponseWrapper(response);
23
24//创建我们的自己的TraceRequest对象
25 CustomerTraceableRequest traceableRequest = new CustomerTraceableRequest(wrapper);
26//创建HttpTrace对象(FilteredTraceableRequest 是内部类,通过Set<Include>筛选那些信息需要展示就保存那些信息),重点设置HttpTrace#Request对象的各种参数
27 HttpTrace httpTrace = httpExchangeTracer.receivedRequest(traceableRequest);
28 try {
29 filterChain.doFilter(wrapper, responseWrapper);
30 } finally {
31//自定义的TraceableResponse 保存需要的response信息
32 CustomerTraceableResponse traceableResponse = new CustomerTraceableResponse(responseWrapper,request);
33//根据Set<Include>设置HttpTrace中session、principal、timeTaken信息以及Response内部类信息
34 this.httpExchangeTracer.sendingResponse(httpTrace, traceableResponse, null, null);
35//将HttpTrace对象保存在Respository中存储起来
36 this.httpTraceRepository.add(httpTrace);
37 }
38 }
39
40 private boolean isRequestValid(HttpServletRequest request) {
41 try {
42 new URI(request.getRequestURL().toString());
43 return true;
44 } catch (URISyntaxException ex) {
45 return false;
46 }
47 }
48
49 @Override
50 public int getOrder() {
51 return Ordered.LOWEST_PRECEDENCE - 10;
52 }
53}
- 第六步:通过
@SpringBootApplication(exclude)禁用HttpTraceAutoConfiguration自动配置,自定义自动配置更换Filter过滤器:
1@Configuration
2@ConditionalOnWebApplication
3@ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
4@EnableConfigurationProperties(HttpTraceProperties.class)
5public class TraceFilterConfig {
6
7//存储HttpTrace信息的对象
8 @Bean
9 @ConditionalOnMissingBean(HttpTraceRepository.class)
10 public InMemoryHttpTraceRepository traceRepository() {
11 return new InMemoryHttpTraceRepository();
12 }
13//创建HttpTrace对象Exchange
14 @Bean
15 @ConditionalOnMissingBean
16 public HttpExchangeTracer httpExchangeTracer(HttpTraceProperties traceProperties) {
17 return new HttpExchangeTracer(traceProperties.getInclude());
18 }
19
20 @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
21 static class ServletTraceFilterConfiguration {
22//将我们自定义的Filter已Bean的方式注册,才能生效
23 @Bean
24 @ConditionalOnMissingBean
25 public CustomerHttpTraceFilter httpTraceFilter(HttpTraceRepository repository,
26 HttpExchangeTracer tracer) {
27 return new CustomerHttpTraceFilter(repository,tracer);
28 }
29 }
30
31 @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
32 static class ReactiveTraceFilterConfiguration {
33
34 @Bean
35 @ConditionalOnMissingBean
36 public HttpTraceWebFilter httpTraceWebFilter(HttpTraceRepository repository,
37 HttpExchangeTracer tracer, HttpTraceProperties traceProperties) {
38 return new HttpTraceWebFilter(repository, tracer,
39 traceProperties.getInclude());
40 }
41 }
42}
集成Redisson健康状态监控
如果有引入spring-boot-starter-redis,SBA默认同过RedisConnectionFactory监控Redis的健康状态,无奈Redisson还没有,自己东收丰衣足食。通过HealthIndicator和ReactiveHealthIndicator使用策略模式实现不同组件的健康监控,后者是使用Rective模式下的。我是通过JavaBean的方式配置Redisson,所以顺便实现ReactiveHealthIndicator再添加该指标即可:
1@Configuration
2@EnableConfigurationProperties(value = RedissonProperties.class)
3public class RedissonConfig implements ReactiveHealthIndicator {
4//自己的RedissonProperties文件
5 @Autowired
6 private RedissonProperties redissonProperties;
7//暴露 redissonClient句柄
8 @Bean
9 @ConditionalOnMissingBean
10 public RedissonClient redisClient() {
11 return Redisson.create(config());
12 }
13//通过Bean的方式配置RedissonConfig相关信息
14 @Bean
15 public Config config() {
16 Config config = new Config();
17 config.useSingleServer() //单实列模式
18 .setAddress(redissonProperties.getAddress() + ":" + redissonProperties.getPort())
19 .setPassword(redissonProperties.getPassword())
20 .setDatabase(redissonProperties.getDatabase())
21 .setConnectionPoolSize(redissonProperties.getConnectionPoolSize())
22 .setConnectionMinimumIdleSize(redissonProperties.getConnectionMinimumIdleSize())
23 .setIdleConnectionTimeout(redissonProperties.getIdleConnectionTimeout())
24 .setSubscriptionConnectionPoolSize(redissonProperties.getSubscriptionConnectionPoolSize())
25 .setSubscriptionConnectionMinimumIdleSize(redissonProperties.getSubscriptionConnectionMinimumIdleSize())
26 .setTimeout(redissonProperties.getTimeout())
27 .setRetryAttempts(redissonProperties.getRetryAttempts())
28 .setRetryInterval(redissonProperties.getRetryInterval())
29 .setConnectTimeout(redissonProperties.getConnectTimeout())
30 .setReconnectionTimeout(redissonProperties.getReconnectionTimeout());
31 config
32 .setCodecProvider(new DefaultCodecProvider())
33 .setEventLoopGroup(new NioEventLoopGroup())
34 .setThreads(Runtime.getRuntime().availableProcessors() * 2)
35 .setNettyThreads(Runtime.getRuntime().availableProcessors() * 2);
36 return config;
37 }
38//实现ReactiveHealthIndicator 重写health方法
39 @Override
40 public Mono<Health> health() {
41 return checkRedissonHealth().onErrorResume(ex -> Mono.just(new Health.Builder().down(ex).build()));
42 }
43//我是通过ping 的方式判断redis服务器是否up的状态,并增加加Netty和Threads的监控
44 private Mono<Health> checkRedissonHealth() {
45 Health.Builder builder = new Health.Builder();
46 builder.withDetail("address", redissonProperties.getAddress());
47 //检测健康状态
48 if (this.redisClient().getNodesGroup().pingAll()) {
49 builder.status(Status.UP);
50 builder.withDetail("dataBase", redissonProperties.getDatabase());
51 builder.withDetail("redisNodeThreads", this.redisClient().getConfig().getThreads());
52 builder.withDetail("nettyThreads", this.redisClient().getConfig().getNettyThreads());
53
54 }else{
55 builder.status(Status.DOWN);
56 }
57 return Mono.just(builder.build());
58 }
59}
结果:
Ok!圆满完成!
如有错误,不吝赐教!